21128: Merge commit 'adbbc9e3c7a36d39b30f403555ee5889e32adcc0' into 21128-toolbar...
authorLisa Knox <lisaknox83@gmail.com>
Fri, 5 Jan 2024 15:47:49 +0000 (10:47 -0500)
committerLisa Knox <lisaknox83@gmail.com>
Fri, 5 Jan 2024 15:47:49 +0000 (10:47 -0500)
Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox@curii.com>

46 files changed:
1  2 
services/workbench2/.yarn/releases/yarn-3.2.0.cjs
services/workbench2/cypress/integration/collection.spec.js
services/workbench2/cypress/integration/multiselect-toolbar.spec.js
services/workbench2/cypress/integration/process.spec.js
services/workbench2/cypress/integration/project.spec.js
services/workbench2/cypress/integration/workflow.spec.js
services/workbench2/src/components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar.tsx
services/workbench2/src/components/data-explorer/data-explorer.tsx
services/workbench2/src/components/data-table/data-table.tsx
services/workbench2/src/components/icon/icon.tsx
services/workbench2/src/components/multiselect-toolbar/MultiselectToolbar.tsx
services/workbench2/src/components/multiselect-toolbar/ms-kind-action-differentiator.ts
services/workbench2/src/components/multiselect-toolbar/ms-toolbar-action-filters.ts
services/workbench2/src/store/collections/collection-info-actions.ts
services/workbench2/src/store/favorites/favorites-actions.ts
services/workbench2/src/store/multiselect/multiselect-actions.tsx
services/workbench2/src/store/multiselect/multiselect-reducer.tsx
services/workbench2/src/store/process-panel/process-panel-actions.ts
services/workbench2/src/store/project-panel/project-panel-middleware-service.ts
services/workbench2/src/store/projects/project-lock-actions.ts
services/workbench2/src/store/public-favorites/public-favorites-actions.ts
services/workbench2/src/store/trash-panel/trash-panel-middleware-service.ts
services/workbench2/src/store/trash/trash-actions.ts
services/workbench2/src/store/workbench/workbench-actions.ts
services/workbench2/src/views-components/context-menu/action-sets/process-resource-action-set.ts
services/workbench2/src/views-components/context-menu/context-menu.tsx
services/workbench2/src/views-components/data-explorer/data-explorer.tsx
services/workbench2/src/views-components/data-explorer/renderers.tsx
services/workbench2/src/views-components/details-panel/details-panel.tsx
services/workbench2/src/views-components/multiselect-toolbar/ms-collection-action-set.ts
services/workbench2/src/views-components/multiselect-toolbar/ms-menu-actions.ts
services/workbench2/src/views-components/multiselect-toolbar/ms-process-action-set.ts
services/workbench2/src/views-components/multiselect-toolbar/ms-project-action-set.ts
services/workbench2/src/views-components/multiselect-toolbar/ms-workflow-action-set.ts
services/workbench2/src/views/all-processes-panel/all-processes-panel.tsx
services/workbench2/src/views/collection-panel/collection-panel.tsx
services/workbench2/src/views/favorite-panel/favorite-panel.tsx
services/workbench2/src/views/process-panel/process-details-attributes.tsx
services/workbench2/src/views/project-panel/project-panel.tsx
services/workbench2/src/views/public-favorites-panel/public-favorites-panel.tsx
services/workbench2/src/views/search-results-panel/search-results-panel.tsx
services/workbench2/src/views/shared-with-me-panel/shared-with-me-panel.tsx
services/workbench2/src/views/subprocess-panel/subprocess-panel-root.tsx
services/workbench2/src/views/subprocess-panel/subprocess-panel.tsx
services/workbench2/src/views/trash-panel/trash-panel.tsx
services/workbench2/src/views/workbench/workbench.tsx

index 59267757f98a302a96d4ff8f5ccd3e48916a930a,b30d0655d004fa0f15cda007116aa609eb7353a1..b30d0655d004fa0f15cda007116aa609eb7353a1
@@@ -470,7 -470,7 +470,7 @@@ Try running the command again with the 
        This command will unset a configuration setting.
      `,examples:[["Unset a simple configuration setting","yarn config unset initScope"],["Unset a complex configuration setting","yarn config unset packageExtensions"],["Unset a nested configuration setting","yarn config unset npmScopes.company.npmRegistryServer"]]});var uae=Am;var KN=ge(require("util")),lm=class extends Le{constructor(){super(...arguments);this.verbose=z.Boolean("-v,--verbose",!1,{description:"Print the setting description on top of the regular key/value information"});this.why=z.Boolean("--why",!1,{description:"Print the reason why a setting is set a particular way"});this.json=z.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins,{strict:!1});return(await Je.start({configuration:e,json:this.json,stdout:this.context.stdout},async i=>{if(e.invalid.size>0&&!this.json){for(let[n,s]of e.invalid)i.reportError($.INVALID_CONFIGURATION_KEY,`Invalid configuration key "${n}" in ${s}`);i.reportSeparator()}if(this.json){let n=Se.sortMap(e.settings.keys(),s=>s);for(let s of n){let o=e.settings.get(s),a=e.getSpecial(s,{hideSecrets:!0,getNativePaths:!0}),l=e.sources.get(s);this.verbose?i.reportJson({key:s,effective:a,source:l}):i.reportJson(N({key:s,effective:a,source:l},o))}}else{let n=Se.sortMap(e.settings.keys(),a=>a),s=n.reduce((a,l)=>Math.max(a,l.length),0),o={breakLength:Infinity,colors:e.get("enableColors"),maxArrayLength:2};if(this.why||this.verbose){let a=n.map(c=>{let u=e.settings.get(c);if(!u)throw new Error(`Assertion failed: This settings ("${c}") should have been registered`);let g=this.why?e.sources.get(c)||"<default>":u.description;return[c,g]}),l=a.reduce((c,[,u])=>Math.max(c,u.length),0);for(let[c,u]of a)i.reportInfo(null,`${c.padEnd(s," ")}   ${u.padEnd(l," ")}   ${(0,KN.inspect)(e.getSpecial(c,{hideSecrets:!0,getNativePaths:!0}),o)}`)}else for(let a of n)i.reportInfo(null,`${a.padEnd(s," ")}   ${(0,KN.inspect)(e.getSpecial(a,{hideSecrets:!0,getNativePaths:!0}),o)}`)}})).exitCode()}};lm.paths=[["config"]],lm.usage=Re.Usage({description:"display the current configuration",details:`
        This command prints the current active configuration settings.
-     `,examples:[["Print the active configuration settings","$0 config"]]});var gae=lm;Es();var HN={};ft(HN,{Strategy:()=>Iu,acceptedStrategies:()=>R8e,dedupe:()=>jN});var fae=ge(ts()),Iu;(function(e){e.HIGHEST="highest"})(Iu||(Iu={}));var R8e=new Set(Object.values(Iu)),F8e={highest:async(t,e,{resolver:r,fetcher:i,resolveOptions:n,fetchOptions:s})=>{let o=new Map;for(let[a,l]of t.storedResolutions){let c=t.storedDescriptors.get(a);if(typeof c=="undefined")throw new Error(`Assertion failed: The descriptor (${a}) should have been registered`);Se.getSetWithDefault(o,c.identHash).add(l)}return Array.from(t.storedDescriptors.values(),async a=>{if(e.length&&!fae.default.isMatch(P.stringifyIdent(a),e))return null;let l=t.storedResolutions.get(a.descriptorHash);if(typeof l=="undefined")throw new Error(`Assertion failed: The resolution (${a.descriptorHash}) should have been registered`);let c=t.originalPackages.get(l);if(typeof c=="undefined"||!r.shouldPersistResolution(c,n))return null;let u=o.get(a.identHash);if(typeof u=="undefined")throw new Error(`Assertion failed: The resolutions (${a.identHash}) should have been registered`);if(u.size===1)return null;let g=[...u].map(y=>{let Q=t.originalPackages.get(y);if(typeof Q=="undefined")throw new Error(`Assertion failed: The package (${y}) should have been registered`);return Q.reference}),f=await r.getSatisfying(a,g,n),h=f==null?void 0:f[0];if(typeof h=="undefined")return null;let p=h.locatorHash,m=t.originalPackages.get(p);if(typeof m=="undefined")throw new Error(`Assertion failed: The package (${p}) should have been registered`);return p===l?null:{descriptor:a,currentPackage:c,updatedPackage:m}})}};async function jN(t,{strategy:e,patterns:r,cache:i,report:n}){let{configuration:s}=t,o=new pi,a=s.makeResolver(),l=s.makeFetcher(),c={cache:i,checksums:t.storedChecksums,fetcher:l,project:t,report:o,skipIntegrityCheck:!0,cacheOptions:{skipIntegrityCheck:!0}},u={project:t,resolver:a,report:o,fetchOptions:c};return await n.startTimerPromise("Deduplication step",async()=>{let f=await F8e[e](t,r,{resolver:a,resolveOptions:u,fetcher:l,fetchOptions:c}),h=Ji.progressViaCounter(f.length);n.reportProgress(h);let p=0;await Promise.all(f.map(Q=>Q.then(S=>{if(S===null)return;p++;let{descriptor:x,currentPackage:M,updatedPackage:Y}=S;n.reportInfo($.UNNAMED,`${P.prettyDescriptor(s,x)} can be deduped from ${P.prettyLocator(s,M)} to ${P.prettyLocator(s,Y)}`),n.reportJson({descriptor:P.stringifyDescriptor(x),currentResolution:P.stringifyLocator(M),updatedResolution:P.stringifyLocator(Y)}),t.storedResolutions.set(x.descriptorHash,Y.locatorHash)}).finally(()=>h.tick())));let m;switch(p){case 0:m="No packages";break;case 1:m="One package";break;default:m=`${p} packages`}let y=ae.pretty(s,e,ae.Type.CODE);return n.reportInfo($.UNNAMED,`${m} can be deduped using the ${y} strategy`),p})}var cm=class extends Le{constructor(){super(...arguments);this.strategy=z.String("-s,--strategy",Iu.HIGHEST,{description:"The strategy to use when deduping dependencies",validator:nn(Iu)});this.check=z.Boolean("-c,--check",!1,{description:"Exit with exit code 1 when duplicates are found, without persisting the dependency tree"});this.json=z.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.mode=z.String("--mode",{description:"Change what artifacts installs generate",validator:nn(di)});this.patterns=z.Rest()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:r}=await ze.find(e,this.context.cwd),i=await Nt.find(e);await r.restoreInstallState({restoreResolutions:!1});let n=0,s=await Je.start({configuration:e,includeFooter:!1,stdout:this.context.stdout,json:this.json},async o=>{n=await jN(r,{strategy:this.strategy,patterns:this.patterns,cache:i,report:o})});return s.hasErrors()?s.exitCode():this.check?n?1:0:(await Je.start({configuration:e,stdout:this.context.stdout,json:this.json},async a=>{await r.install({cache:i,report:a,mode:this.mode})})).exitCode()}};cm.paths=[["dedupe"]],cm.usage=Re.Usage({description:"deduplicate dependencies with overlapping ranges",details:"\n      Duplicates are defined as descriptors with overlapping ranges being resolved and locked to different locators. They are a natural consequence of Yarn's deterministic installs, but they can sometimes pile up and unnecessarily increase the size of your project.\n\n      This command dedupes dependencies in the current project using different strategies (only one is implemented at the moment):\n\n      - `highest`: Reuses (where possible) the locators with the highest versions. This means that dependencies can only be upgraded, never downgraded. It's also guaranteed that it never takes more than a single pass to dedupe the entire dependency tree.\n\n      **Note:** Even though it never produces a wrong dependency tree, this command should be used with caution, as it modifies the dependency tree, which can sometimes cause problems when packages don't strictly follow semver recommendations. Because of this, it is recommended to also review the changes manually.\n\n      If set, the `-c,--check` flag will only report the found duplicates, without persisting the modified dependency tree. If changes are found, the command will exit with a non-zero exit code, making it suitable for CI purposes.\n\n      If the `--mode=<mode>` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\n\n      - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the later will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\n\n      - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\n\n      This command accepts glob patterns as arguments (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n\n      ### In-depth explanation:\n\n      Yarn doesn't deduplicate dependencies by default, otherwise installs wouldn't be deterministic and the lockfile would be useless. What it actually does is that it tries to not duplicate dependencies in the first place.\n\n      **Example:** If `foo@^2.3.4` (a dependency of a dependency) has already been resolved to `foo@2.3.4`, running `yarn add foo@*`will cause Yarn to reuse `foo@2.3.4`, even if the latest `foo` is actually `foo@2.10.14`, thus preventing unnecessary duplication.\n\n      Duplication happens when Yarn can't unlock dependencies that have already been locked inside the lockfile.\n\n      **Example:** If `foo@^2.3.4` (a dependency of a dependency) has already been resolved to `foo@2.3.4`, running `yarn add foo@2.10.14` will cause Yarn to install `foo@2.10.14` because the existing resolution doesn't satisfy the range `2.10.14`. This behavior can lead to (sometimes) unwanted duplication, since now the lockfile contains 2 separate resolutions for the 2 `foo` descriptors, even though they have overlapping ranges, which means that the lockfile can be simplified so that both descriptors resolve to `foo@2.10.14`.\n    ",examples:[["Dedupe all packages","$0 dedupe"],["Dedupe all packages using a specific strategy","$0 dedupe --strategy highest"],["Dedupe a specific package","$0 dedupe lodash"],["Dedupe all packages with the `@babel/*` scope","$0 dedupe '@babel/*'"],["Check for duplicates (can be used as a CI step)","$0 dedupe --check"]]});var hae=cm;var ib=class extends Le{async execute(){let{plugins:e}=await ye.find(this.context.cwd,this.context.plugins),r=[];for(let o of e){let{commands:a}=o[1];if(a){let c=Is.from(a).definitions();r.push([o[0],c])}}let i=this.cli.definitions(),n=(o,a)=>o.split(" ").slice(1).join()===a.split(" ").slice(1).join(),s=dae()["@yarnpkg/builder"].bundles.standard;for(let o of r){let a=o[1];for(let l of a)i.find(c=>n(c.path,l.path)).plugin={name:o[0],isDefault:s.includes(o[0])}}this.context.stdout.write(`${JSON.stringify(i,null,2)}
+     `,examples:[["Print the active configuration settings","$0 config"]]});var gae=lm;Es();var HN={};ft(HN,{Strategy:()=>Iu,acceptedStrategies:()=>R8e,dedupe:()=>jN});var fae=ge(ts()),Iu;(function(e){e.HIGHEST="highest"})(Iu||(Iu={}));var R8e=new Set(Object.values(Iu)),F8e={highest:async(t,e,{resolver:r,fetcher:i,resolveOptions:n,fetchOptions:s})=>{let o=new Map;for(let[a,l]of t.storedResolutions){let c=t.storedDescriptors.get(a);if(typeof c=="undefined")throw new Error(`Assertion failed: The descriptor (${a}) should have been registered`);Se.getSetWithDefault(o,c.identHash).add(l)}return Array.from(t.storedDescriptors.values(),async a=>{if(e.length&&!fae.default.isMatch(P.stringifyIdent(a),e))return null;let l=t.storedResolutions.get(a.descriptorHash);if(typeof l=="undefined")throw new Error(`Assertion failed: The resolution (${a.descriptorHash}) should have been registered`);let c=t.originalPackages.get(l);if(typeof c=="undefined"||!r.shouldPersistResolution(c,n))return null;let u=o.get(a.identHash);if(typeof u=="undefined")throw new Error(`Assertion failed: The resolutions (${a.identHash}) should have been registered`);if(u.size===1)return null;let g=[...u].map(y=>{let Q=t.originalPackages.get(y);if(typeof Q=="undefined")throw new Error(`Assertion failed: The package (${y}) should have been registered`);return Q.reference}),f=await r.getSatisfying(a,g,n),h=f==null?void 0:f[0];if(typeof h=="undefined")return null;let p=h.locatorHash,m=t.originalPackages.get(p);if(typeof m=="undefined")throw new Error(`Assertion failed: The package (${p}) should have been registered`);return p===l?null:{descriptor:a,currentPackage:c,updatedPackage:m}})}};async function jN(t,{strategy:e,patterns:r,cache:i,report:n}){let{configuration:s}=t,o=new pi,a=s.makeResolver(),l=s.makeFetcher(),c={cache:i,checksums:t.storedChecksums,fetcher:l,project:t,report:o,skipIntegrityCheck:!0,cacheOptions:{skipIntegrityCheck:!0}},u={project:t,resolver:a,report:o,fetchOptions:c};return await n.startTimerPromise("Deduplication step",async()=>{let f=await F8e[e](t,r,{resolver:a,resolveOptions:u,fetcher:l,fetchOptions:c}),h=Ji.progressViaCounter(f.length);n.reportProgress(h);let p=0;await Promise.all(f.map(Q=>Q.then(S=>{if(S===null)return;p++;let{descriptor:x,currentPackage:M,updatedPackage:Y}=S;n.reportInfo($.UNNAMED,`${P.prettyDescriptor(s,x)} can be deduped from ${P.prettyLocator(s,M)} to ${P.prettyLocator(s,Y)}`),n.reportJson({descriptor:P.stringifyDescriptor(x),currentResolution:P.stringifyLocator(M),updatedResolution:P.stringifyLocator(Y)}),t.storedResolutions.set(x.descriptorHash,Y.locatorHash)}).finally(()=>h.tick())));let m;switch(p){case 0:m="No packages";break;case 1:m="One package";break;default:m=`${p} packages`}let y=ae.pretty(s,e,ae.Type.CODE);return n.reportInfo($.UNNAMED,`${m} can be deduped using the ${y} strategy`),p})}var cm=class extends Le{constructor(){super(...arguments);this.strategy=z.String("-s,--strategy",Iu.HIGHEST,{description:"The strategy to use when deduping dependencies",validator:nn(Iu)});this.check=z.Boolean("-c,--check",!1,{description:"Exit with exit code 1 when duplicates are found, without persisting the dependency tree"});this.json=z.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.mode=z.String("--mode",{description:"Change what artifacts installs generate",validator:nn(di)});this.patterns=z.Rest()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:r}=await ze.find(e,this.context.cwd),i=await Nt.find(e);await r.restoreInstallState({restoreResolutions:!1});let n=0,s=await Je.start({configuration:e,includeFooter:!1,stdout:this.context.stdout,json:this.json},async o=>{n=await jN(r,{strategy:this.strategy,patterns:this.patterns,cache:i,report:o})});return s.hasErrors()?s.exitCode():this.check?n?1:0:(await Je.start({configuration:e,stdout:this.context.stdout,json:this.json},async a=>{await r.install({cache:i,report:a,mode:this.mode})})).exitCode()}};cm.paths=[["dedupe"]],cm.usage=Re.Usage({description:"deduplicate dependencies with overlapping ranges",details:"\n      Duplicates are defined as descriptors with overlapping ranges being resolved and locked to different locators. They are a natural consequence of Yarn's deterministic installs, but they can sometimes pile up and unnecessarily increase the size of your project.\n\n      This command dedupes dependencies in the current project using different strategies (only one is implemented at the moment):\n\n      - `highest`: Reuses (where possible) the locators with the highest versions. This means that dependencies can only be upgraded, never downgraded. It's also guaranteed that it never takes more than a single pass to dedupe the entire dependency tree.\n\n      **Note:** Even though it never produces a wrong dependency tree, this command should be used with caution, as it modifies the dependency tree, which can sometimes cause problems when packages don't strictly follow semver recommendations. Because of this, it is recommended to also review the changes manually.\n\n      If set, the `-c,--check` flag will only report the found duplicates, without persisting the modified dependency tree. If changes are found, the command will exit with a non-zero exit code, making it suitable for CI purposes.\n\n      If the `--mode=<mode>` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\n\n      - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the later will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\n\n      - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\n\n      This command accepts glob patterns as arguments (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n\n      ### In-depth explanation:\n\n      Yarn doesn't deduplicate dependencies by default, otherwise installs wouldn't be deterministic and the lockfile would be useless. What it actually does is that it tries to not duplicate dependencies in the first place.\n\n      **Example:** If `foo@^2.3.4` (a dependency of a dependency) has already been resolved to `foo@2.3.4`, running `yarn add foo@*`will cause Yarn to reuse `foo@2.3.4`, even if the latest `foo` is actually `foo@2.10.14`, thus preventing unnecessary duplication.\n\n      Duplication happens when Yarn can't unlock dependencies that have already been locked inside the lockfile.\n\n      **Example:** If `foo@^2.3.4` (a dependency of a dependency) has already been resolved to `foo@2.3.4`, running `yarn add foo@2.10.14` will cause Yarn to install `foo@2.10.14` because the existing resolution doesn't satisfy the range `2.10.14`. This behavior can lead to (sometimes) unwanted duplication, since now the lockfile contains 2 separate resolutions for the 2 `foo` descriptors, even though they have overlapping ranges, which means that the lockfile can be simplified so that both descriptors resolve to `foo@2.10.14`.\n    ",examples:[["Dedupe all packages","$0 dedupe"],["Dedupe all packages using a specific strategy","$0 dedupe --strategy highest"],["Dedupe a specific package","$0 dedupe lodash"],["Dedupe all packages with the `@babel/*` scope","$0 dedupe '@babel/*'"],["Check for duplicates (can be used as a CI step)","$0 dedupe --check"]]});var hae=cm;var ib=class extends Le{async execute(){let{plugins:e}=await ye.find(this.context.cwd,this.context.plugins),r=[];for(let o of e){let{commands:a}=o[1];if(a){let c=Is.from(a).definitions();r.push([o[0],c])}}let i=this.cli.definitions(),n=(o,a)=>o.split(" ").slice(1).join()===a.split(" ").slice(1).join(),s=dae()["@yarnpkg/builder"].bundles.standard;for(let o of r){let a=o[1];for(let l of a)i.find(c=>n(c.path,l.path)).plugin={name:o[0],useAlts:s.includes(o[0])}}this.context.stdout.write(`${JSON.stringify(i,null,2)}
  `)}};ib.paths=[["--clipanion=definitions"]];var Cae=ib;var nb=class extends Le{async execute(){this.context.stdout.write(this.cli.usage(null))}};nb.paths=[["help"],["--help"],["-h"]];var mae=nb;var GN=class extends Le{constructor(){super(...arguments);this.leadingArgument=z.String();this.args=z.Proxy()}async execute(){if(this.leadingArgument.match(/[\\/]/)&&!P.tryParseIdent(this.leadingArgument)){let e=k.resolve(this.context.cwd,j.toPortablePath(this.leadingArgument));return await this.cli.run(this.args,{cwd:e})}else return await this.cli.run(["run",this.leadingArgument,...this.args])}},Eae=GN;var sb=class extends Le{async execute(){this.context.stdout.write(`${Ur||"<unknown>"}
  `)}};sb.paths=[["-v"],["--version"]];var Iae=sb;var um=class extends Le{constructor(){super(...arguments);this.commandName=z.String();this.args=z.Proxy()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:r,locator:i}=await ze.find(e,this.context.cwd);return await r.restoreInstallState(),await Zt.executePackageShellcode(i,this.commandName,this.args,{cwd:this.context.cwd,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr,project:r})}};um.paths=[["exec"]],um.usage=Re.Usage({description:"execute a shell script",details:`
        This command simply executes a shell script within the context of the root directory of the active workspace using the portable shell.
index f83a974193abe98926bb982ce4d731446ebd9bb3,54c570f7c4453fdafa3fe5bd4cd27795eadcb1e1..54c570f7c4453fdafa3fe5bd4cd27795eadcb1e1
@@@ -32,6 -32,45 +32,45 @@@ describe("Collection panel tests", func
          cy.clearLocalStorage();
      });
  
+     it('shows the appropriate buttons in the toolbar', () => {
+         const msButtonTooltips = [
+             'API Details',
+             'Add to Favorites',
+             'Copy to clipboard',
+             'Edit collection',
+             'Make a copy',
+             'Move to',
+             'Move to trash',
+             'Open in new tab',
+             'Open with 3rd party client',
+             'Share',
+             'View details',
+         ];
+         cy.loginAs(activeUser);
+         const name = `Test collection ${Math.floor(Math.random() * 999999)}`;
+         cy.get("[data-cy=side-panel-button]").click({force: true});
+         cy.get("[data-cy=side-panel-new-collection]").click();
+         cy.get("[data-cy=form-dialog]")
+             .should("contain", "New collection")
+             .within(() => {
+                 cy.get("[data-cy=name-field]").within(() => {
+                     cy.get("input").type(name);
+                 });
+                 cy.get("[data-cy=form-submit-btn]").click();
+             });
+             cy.get("[data-cy=side-panel-tree]").contains("Home Projects").click();
+             cy.waitForDom()
+             cy.get('[data-cy=data-table-row]').contains(name).should('exist').parent().parent().parent().parent().click()
+             cy.get('[data-cy=multiselect-button]').should('have.length', msButtonTooltips.length)
+             for (let i = 0; i < msButtonTooltips.length; i++) {
+                 cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseover');
+                 cy.get('body').contains(msButtonTooltips[i]).should('exist')
+                 cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseout');
+             }
+     })
      it("allows to download mountain duck config for a collection", () => {
          cy.createCollection(adminUser.token, {
              name: `Test collection ${Math.floor(Math.random() * 999999)}`,
          cy.get("[data-cy=form-dialog]").should("exist").and("contain", "Collection with the same name already exists");
      });
  
+     
      it("uses the property editor (from edit dialog) with vocabulary terms", function () {
          cy.createCollection(adminUser.token, {
              name: `Test collection ${Math.floor(Math.random() * 999999)}`,
              });
      });
  
+     
      it("uses the editor (from details panel) with vocabulary terms", function () {
          cy.createCollection(adminUser.token, {
              name: `Test collection ${Math.floor(Math.random() * 999999)}`,
index 0000000000000000000000000000000000000000,ef503f7ef628874af301821f23a1ffc1385e39e0..ef503f7ef628874af301821f23a1ffc1385e39e0
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,36 +1,36 @@@
+ // Copyright (C) The Arvados Authors. All rights reserved.
+ //
+ // SPDX-License-Identifier: AGPL-3.0
+ describe('Multiselect Toolbar Tests', () => {
+     let activeUser;
+     let adminUser;
+     before(function () {
+         // Only set up common users once. These aren't set up as aliases because
+         // aliases are cleaned up after every test. Also it doesn't make sense
+         // to set the same users on beforeEach() over and over again, so we
+         // separate a little from Cypress' 'Best Practices' here.
+         cy.getUser('admin', 'Admin', 'User', true, true)
+             .as('adminUser')
+             .then(function () {
+                 adminUser = this.adminUser;
+             });
+         cy.getUser('user', 'Active', 'User', false, true)
+             .as('activeUser')
+             .then(function () {
+                 activeUser = this.activeUser;
+             });
+     });
+     beforeEach(function () {
+         cy.clearCookies();
+         cy.clearLocalStorage();
+     });
+     it('exists in DOM in neutral state', () => {
+         cy.loginAs(activeUser);
+         cy.get('[data-cy=multiselect-toolbar]').should('exist');
+         cy.get('[data-cy=multiselect-button]').should('not.exist');
+     });
+ });
index 38d9794b18573a0379a3bbba93f53f0b661abe83,9ea026b9906511c297cb6367370b8c08955f8869..9ea026b9906511c297cb6367370b8c08955f8869
@@@ -90,6 -90,49 +90,49 @@@ describe("Process tests", function () 
          });
      }
  
+     describe('Multiselect Toolbar', () => {
+         it('shows the appropriate buttons in the toolbar', () => {
+             const msButtonTooltips = [
+                 'API Details',
+                 'Add to Favorites',
+                 'CANCEL',
+                 'Copy and re-run process',
+                 'Edit process',
+                 'Move to',
+                 'Open in new tab',
+                 'Outputs',
+                 'Remove',
+                 'Share',
+                 'View details',
+             ];
+     
+             createContainerRequest(
+                 activeUser,
+                 `test_container_request ${Math.floor(Math.random() * 999999)}`,
+                 "arvados/jobs",
+                 ["echo", "hello world"],
+                 false,
+                 "Committed"
+             ).then(function (containerRequest) {
+                 cy.loginAs(activeUser);
+                 cy.goToPath(`/processes/${containerRequest.uuid}`);
+                 cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
+                 cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`Active User (${activeUser.user.uuid})`);
+                 cy.get("[data-cy=process-details-attributes-runtime-user]").should("not.exist");
+                 cy.get("[data-cy=side-panel-tree]").contains("Home Projects").click();
+                 cy.waitForDom()
+                 cy.get('[data-cy=data-table-row]').contains(containerRequest.name).should('exist').parent().parent().parent().parent().click()
+                 cy.get('[data-cy=multiselect-button]').should('have.length', msButtonTooltips.length)
+                 for (let i = 0; i < msButtonTooltips.length; i++) {
+                     cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseover');
+                     cy.get('body').contains(msButtonTooltips[i]).should('exist')
+                     cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseout');
+                 }
+             });
+         })
+     })
      describe("Details panel", function () {
          it("shows process details", function () {
              createContainerRequest(
index e61138219dcf01558151f2179aec78983c73f6ad,e6185c108e94c454970ad2605d83f3bc8b4637a2..e6185c108e94c454970ad2605d83f3bc8b4637a2
@@@ -180,6 -180,47 +180,47 @@@ describe("Project tests", function () 
          verifyProjectDescription(projName, null);
      });
  
+     it('shows the appropriate buttons in the multiselect toolbar', () => {
+         const msButtonTooltips = [
+             'API Details',
+             'Add to Favorites',
+             'Copy to clipboard',
+             'Edit project',
+             'Freeze Project',
+             'Move to',
+             'Move to trash',
+             'New project',
+             'Open in new tab',
+             'Open with 3rd party client',
+             'Share',
+             'View details',
+         ];
+         cy.loginAs(activeUser);
+         const projName = `Test project (${Math.floor(999999 * Math.random())})`;
+         cy.get('[data-cy=side-panel-button]').click();
+         cy.get('[data-cy=side-panel-new-project]').click();
+         cy.get('[data-cy=form-dialog]')
+             .should('contain', 'New Project')
+             .within(() => {
+                 cy.get('[data-cy=name-field]').within(() => {
+                     cy.get('input').type(projName);
+                 });
+             })
+         cy.get("[data-cy=form-submit-btn]").click();
+         cy.waitForDom()
+         cy.go('back')
+         cy.get('[data-cy=data-table-row]').contains(projName).should('exist').parent().parent().parent().click()
+         cy.get('[data-cy=multiselect-button]').should('have.length', msButtonTooltips.length)
+         for (let i = 0; i < msButtonTooltips.length; i++) {
+             cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseover');
+             cy.get('body').contains(msButtonTooltips[i]).should('exist')
+             cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseout');
+         }
+     })
      it("creates new project on home project and then a subproject inside it", function () {
          const createProject = function (name, parentName) {
              cy.get("[data-cy=side-panel-button]").click();
              });
          });
  
-         it("should be able to froze own project", () => {
+         it("should be able to freeze own project", () => {
              cy.getAll("@mainProject").then(([mainProject]) => {
                  cy.loginAs(activeUser);
  
              });
          });
  
-         it("should be able to froze not owned project", () => {
+         it("should be able to freeze not owned project", () => {
              cy.getAll("@adminProject").then(([adminProject]) => {
                  cy.loginAs(activeUser);
  
              });
          });
  
-         it("should be able to unfroze project if user is an admin", () => {
+         it("should be able to unfreeze project if user is an admin", () => {
              cy.getAll("@adminProject").then(([adminProject]) => {
                  cy.loginAs(adminUser);
  
index 76ad3c631dfe96fb33b836fb83f348b446aa293a,844e87d8deb64b1969014296330a17312623051f..844e87d8deb64b1969014296330a17312623051f
@@@ -265,4 -265,31 +265,31 @@@ describe('Registered workflow panel tes
                      });
              });
      });
+     it('shows the appropriate buttons in the multiselect toolbar', () => {
+         const msButtonTooltips = [
+             'API Details',
+             'Copy to clipboard',
+             'Delete Workflow',
+             'Open in new tab',
+             'Run Workflow',
+             'View details',
+         ];
+         cy.createResource(activeUser.token, "workflows", {workflow: {name: "Test wf"}})
+             .then(function(workflowResource) {
+                 cy.loginAs(activeUser);
+                 cy.get("[data-cy=side-panel-tree]").contains("Home Projects").click();
+                 cy.waitForDom()
+                 cy.get('[data-cy=data-table-row]').contains(workflowResource.name).should('exist').parent().parent().parent().click()
+                 cy.get('[data-cy=multiselect-button]').should('have.length', msButtonTooltips.length)
+                 for (let i = 0; i < msButtonTooltips.length; i++) {
+                         cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseover');
+                         cy.get('body').contains(msButtonTooltips[i]).should('exist')
+                         cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseout');
+                     }
+                 });
+     })
  });
index 586bb13b1e8e43b520b9848d16cbc7483ae79709,3ef483dfe03faf63edd24fa3f02ff322e16110c4..3ef483dfe03faf63edd24fa3f02ff322e16110c4
@@@ -48,7 -48,7 +48,7 @@@ export const CopyToClipboardSnackbar = 
              render() {
                  const { children, value, classes } = this.props;
                  return (
-                     <Tooltip title='Copy to clipboard'>
+                     <Tooltip title='Copy to clipboard' onClick={(ev) => ev.stopPropagation()}>
                          <span className={classes.copyIcon}>
                              <CopyToClipboard text={value} onCopy={this.onCopy}>
                                  {children || <CopyIcon />}
index b3a93bbb799c9a64cfcee18602271606fc99e912,27e46d584962c8d3e1cb1ca536b21ab1b4577ecf..27e46d584962c8d3e1cb1ca536b21ab1b4577ecf
@@@ -390,7 -390,10 +390,10 @@@ export const DataExplorer = withStyles(
                  >
                      <IconButton
                          className={this.props.classes.moreOptionsButton}
-                         onClick={event => this.props.onContextMenu(event, item)}
+                         onClick={event => {
+                             event.stopPropagation()
+                             this.props.onContextMenu(event, item)
+                         }}
                      >
                          <MoreVerticalIcon />
                      </IconButton>
index 155d772f85855ddd86a90ecc7842065d543e830f,de3e272d1ed88f8ec2d622222bc390b61e720dad..de3e272d1ed88f8ec2d622222bc390b61e720dad
@@@ -144,7 -144,7 +144,7 @@@ export const DataTable = withStyles(sty
          };
  
          componentDidMount(): void {
-             this.initializeCheckedList(this.props.items);
+             this.initializeCheckedList([]);
          }
  
          componentDidUpdate(prevProps: Readonly<DataTableProps<T>>, prevState: DataTableState) {
                  if (items.length) this.initializeCheckedList(items);
                  else setCheckedListOnStore({});
              }
+             if (prevProps.currentRoute !== this.props.currentRoute) {
+                 this.initializeCheckedList([])
+             }
+         }
+         componentWillUnmount(): void {
+             this.initializeCheckedList([])
          }
  
          checkBoxColumn: DataColumn<any, any> = {
                  const { classes, checkedList } = this.props;
                  return (
                      <input
+                         data-cy={`multiselect-checkbox-${uuid}`}
                          type="checkbox"
                          name={uuid}
                          className={classes.checkBox}
              const { onRowClick, onRowDoubleClick, extractKey, classes, currentItemUuid, currentRoute } = this.props;
              return (
                  <TableRow
+                     data-cy={'data-table-row'}
                      hover
                      key={extractKey ? extractKey(item) : index}
                      onClick={event => onRowClick && onRowClick(event, item)}
index e29930a4b9974b84a54370f7c61b2ee726d27b41,0000000000000000000000000000000000000000..8ec4c59b8781e2b4dc63d04b62bff4f955a815a9
mode 100644,000000..100644
--- /dev/null
@@@ -1,269 -1,0 +1,269 @@@
- export const FreezeIcon = (props: any) => (
 +// Copyright (C) The Arvados Authors. All rights reserved.
 +//
 +// SPDX-License-Identifier: AGPL-3.0
 +
 +import React from "react";
 +import { Badge, SvgIcon, Tooltip } from "@material-ui/core";
 +import Add from "@material-ui/icons/Add";
 +import ArrowBack from "@material-ui/icons/ArrowBack";
 +import ArrowDropDown from "@material-ui/icons/ArrowDropDown";
 +import Build from "@material-ui/icons/Build";
 +import Cached from "@material-ui/icons/Cached";
 +import DescriptionIcon from "@material-ui/icons/Description";
 +import ChevronLeft from "@material-ui/icons/ChevronLeft";
 +import CloudUpload from "@material-ui/icons/CloudUpload";
 +import Code from "@material-ui/icons/Code";
 +import Create from "@material-ui/icons/Create";
 +import ImportContacts from "@material-ui/icons/ImportContacts";
 +import ChevronRight from "@material-ui/icons/ChevronRight";
 +import Close from "@material-ui/icons/Close";
 +import ContentCopy from "@material-ui/icons/FileCopyOutlined";
 +import CreateNewFolder from "@material-ui/icons/CreateNewFolder";
 +import Delete from "@material-ui/icons/Delete";
 +import DeviceHub from "@material-ui/icons/DeviceHub";
 +import Edit from "@material-ui/icons/Edit";
 +import ErrorRoundedIcon from "@material-ui/icons/ErrorRounded";
 +import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
 +import FlipToFront from "@material-ui/icons/FlipToFront";
 +import Folder from "@material-ui/icons/Folder";
 +import FolderShared from "@material-ui/icons/FolderShared";
 +import Pageview from "@material-ui/icons/Pageview";
 +import GetApp from "@material-ui/icons/GetApp";
 +import Help from "@material-ui/icons/Help";
 +import HelpOutline from "@material-ui/icons/HelpOutline";
 +import History from "@material-ui/icons/History";
 +import Inbox from "@material-ui/icons/Inbox";
 +import Memory from "@material-ui/icons/Memory";
 +import MoveToInbox from "@material-ui/icons/MoveToInbox";
 +import Info from "@material-ui/icons/Info";
 +import Input from "@material-ui/icons/Input";
 +import InsertDriveFile from "@material-ui/icons/InsertDriveFile";
 +import LastPage from "@material-ui/icons/LastPage";
 +import LibraryBooks from "@material-ui/icons/LibraryBooks";
 +import ListAlt from "@material-ui/icons/ListAlt";
 +import Menu from "@material-ui/icons/Menu";
 +import MoreVert from "@material-ui/icons/MoreVert";
 +import MoreHoriz from "@material-ui/icons/MoreHoriz";
 +import Mail from "@material-ui/icons/Mail";
 +import Notifications from "@material-ui/icons/Notifications";
 +import OpenInNew from "@material-ui/icons/OpenInNew";
 +import People from "@material-ui/icons/People";
 +import Person from "@material-ui/icons/Person";
 +import PersonAdd from "@material-ui/icons/PersonAdd";
 +import PlayArrow from "@material-ui/icons/PlayArrow";
 +import Public from "@material-ui/icons/Public";
 +import RateReview from "@material-ui/icons/RateReview";
 +import RestoreFromTrash from "@material-ui/icons/History";
 +import Search from "@material-ui/icons/Search";
 +import SettingsApplications from "@material-ui/icons/SettingsApplications";
 +import SettingsEthernet from "@material-ui/icons/SettingsEthernet";
 +import Settings from "@material-ui/icons/Settings";
 +import Star from "@material-ui/icons/Star";
 +import StarBorder from "@material-ui/icons/StarBorder";
 +import Warning from "@material-ui/icons/Warning";
 +import VpnKey from "@material-ui/icons/VpnKey";
 +import LinkOutlined from "@material-ui/icons/LinkOutlined";
 +import RemoveRedEye from "@material-ui/icons/RemoveRedEye";
 +import Computer from "@material-ui/icons/Computer";
 +import WrapText from "@material-ui/icons/WrapText";
 +import TextIncrease from "@material-ui/icons/ZoomIn";
 +import TextDecrease from "@material-ui/icons/ZoomOut";
 +import FullscreenSharp from "@material-ui/icons/FullscreenSharp";
 +import FullscreenExitSharp from "@material-ui/icons/FullscreenExitSharp";
 +import ExitToApp from "@material-ui/icons/ExitToApp";
 +import CheckCircleOutline from "@material-ui/icons/CheckCircleOutline";
 +import RemoveCircleOutline from "@material-ui/icons/RemoveCircleOutline";
 +import NotInterested from "@material-ui/icons/NotInterested";
 +import Image from "@material-ui/icons/Image";
 +import Stop from "@material-ui/icons/Stop";
 +import FileCopy from "@material-ui/icons/FileCopy";
 +
 +// 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, faUsers, faEllipsisH);
 +
- export const UnfreezeIcon = (props: any) => (
++export const FreezeIcon: IconType = (props: any) => (
 +    <SvgIcon {...props}>
 +        <path d="M20.79,13.95L18.46,14.57L16.46,13.44V10.56L18.46,9.43L20.79,10.05L21.31,8.12L19.54,7.65L20,5.88L18.07,5.36L17.45,7.69L15.45,8.82L13,7.38V5.12L14.71,3.41L13.29,2L12,3.29L10.71,2L9.29,3.41L11,5.12V7.38L8.5,8.82L6.5,7.69L5.92,5.36L4,5.88L4.47,7.65L2.7,8.12L3.22,10.05L5.55,9.43L7.55,10.56V13.45L5.55,14.58L3.22,13.96L2.7,15.89L4.47,16.36L4,18.12L5.93,18.64L6.55,16.31L8.55,15.18L11,16.62V18.88L9.29,20.59L10.71,22L12,20.71L13.29,22L14.7,20.59L13,18.88V16.62L15.5,15.17L17.5,16.3L18.12,18.63L20,18.12L19.53,16.35L21.3,15.88L20.79,13.95M9.5,10.56L12,9.11L14.5,10.56V13.44L12,14.89L9.5,13.44V10.56Z" />
 +    </SvgIcon>
 +);
 +
++export const UnfreezeIcon: IconType = (props: any) => (
 +    <SvgIcon {...props}>
 +        <path d="M11 5.12L9.29 3.41L10.71 2L12 3.29L13.29 2L14.71 3.41L13 5.12V7.38L15.45 8.82L17.45 7.69L18.07 5.36L20 5.88L19.54 7.65L21.31 8.12L20.79 10.05L18.46 9.43L16.46 10.56V13.26L14.5 11.3V10.56L12.74 9.54L10.73 7.53L11 7.38V5.12M18.46 14.57L16.87 13.67L19.55 16.35L21.3 15.88L20.79 13.95L18.46 14.57M13 16.62V18.88L14.7 20.59L13.29 22L12 20.71L10.71 22L9.29 20.59L11 18.88V16.62L8.55 15.18L6.55 16.31L5.93 18.64L4 18.12L4.47 16.36L2.7 15.89L3.22 13.96L5.55 14.58L7.55 13.45V10.56L5.55 9.43L3.22 10.05L2.7 8.12L4.47 7.65L4 5.89L1.11 3L2.39 1.73L22.11 21.46L20.84 22.73L14.1 16L13 16.62M12 14.89L12.63 14.5L9.5 11.39V13.44L12 14.89Z" />
 +    </SvgIcon>
 +);
 +
 +export const PendingIcon = (props: any) => (
 +    <span {...props}>
 +        <span className="fas fa-ellipsis-h" />
 +    </span>
 +);
 +
 +export const ReadOnlyIcon = (props: any) => (
 +    <span {...props}>
 +        <div className="fa-layers fa-1x fa-fw">
 +            <span
 +                className="fas fa-slash"
 +                data-fa-mask="fas fa-pencil-alt"
 +                data-fa-transform="down-1.5"
 +            />
 +            <span className="fas fa-slash" />
 +        </div>
 +    </span>
 +);
 +
 +export const GroupsIcon = (props: any) => (
 +    <span {...props}>
 +        <span className="fas fa-users" />
 +    </span>
 +);
 +
 +export const CollectionOldVersionIcon = (props: any) => (
 +    <Tooltip title="Old version">
 +        <Badge badgeContent={<History fontSize="small" />}>
 +            <CollectionIcon {...props} />
 +        </Badge>
 +    </Tooltip>
 +);
 +
 +// https://materialdesignicons.com/icon/image-off
 +export const ImageOffIcon = (props: any) => (
 +    <SvgIcon {...props}>
 +        <path d="M21 17.2L6.8 3H19C20.1 3 21 3.9 21 5V17.2M20.7 22L19.7 21H5C3.9 21 3 20.1 3 19V4.3L2 3.3L3.3 2L22 20.7L20.7 22M16.8 18L12.9 14.1L11 16.5L8.5 13.5L5 18H16.8Z" />
 +    </SvgIcon>
 +);
 +
 +// https://materialdesignicons.com/icon/inbox-arrow-up
 +export const OutputIcon: IconType = (props: any) => (
 +    <SvgIcon {...props}>
 +        <path d="M14,14H10V11H8L12,7L16,11H14V14M16,11M5,15V5H19V15H15A3,3 0 0,1 12,18A3,3 0 0,1 9,15H5M19,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3" />
 +    </SvgIcon>
 +);
 +
 +// https://pictogrammers.com/library/mdi/icon/file-move/
 +export const FileMoveIcon: IconType = (props: any) => (
 +    <SvgIcon {...props}>
 +        <path d="M14,17H18V14L23,18.5L18,23V20H14V17M13,9H18.5L13,3.5V9M6,2H14L20,8V12.34C19.37,12.12 18.7,12 18,12A6,6 0 0,0 12,18C12,19.54 12.58,20.94 13.53,22H6C4.89,22 4,21.1 4,20V4A2,2 0 0,1 6,2Z" />
 +    </SvgIcon>
 +);
 +
 +// https://pictogrammers.com/library/mdi/icon/checkbox-multiple-outline/
 +export const CheckboxMultipleOutline: IconType = (props: any) => (
 +    <SvgIcon {...props}>
 +        <path d="M20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2M20,16H8V4H20V16M16,20V22H4A2,2 0 0,1 2,20V7H4V20H16M18.53,8.06L17.47,7L12.59,11.88L10.47,9.76L9.41,10.82L12.59,14L18.53,8.06Z" />
 +    </SvgIcon>
 +);
 +
 +// https://pictogrammers.com/library/mdi/icon/checkbox-multiple-blank-outline/
 +export const CheckboxMultipleBlankOutline: IconType = (props: any) => (
 +    <SvgIcon {...props}>
 +        <path d="M20,16V4H8V16H20M22,16A2,2 0 0,1 20,18H8C6.89,18 6,17.1 6,16V4C6,2.89 6.89,2 8,2H20A2,2 0 0,1 22,4V16M16,20V22H4A2,2 0 0,1 2,20V7H4V20H16Z" />
 +    </SvgIcon>
 +);
 +
 +//https://pictogrammers.com/library/mdi/icon/console/
 +export const TerminalIcon: IconType = (props: any) => (
 +    <SvgIcon {...props}>
 +        <path d="M20,19V7H4V19H20M20,3A2,2 0 0,1 22,5V19A2,2 0 0,1 20,21H4A2,2 0 0,1 2,19V5C2,3.89 2.9,3 4,3H20M13,17V15H18V17H13M9.58,13L5.57,9H8.4L11.7,12.3C12.09,12.69 12.09,13.33 11.7,13.72L8.42,17H5.59L9.58,13Z" />
 +    </SvgIcon>
 +)
 +
 +export type IconType = React.SFC<{ className?: string; style?: object }>;
 +
 +export const AddIcon: IconType = props => <Add {...props} />;
 +export const AddFavoriteIcon: IconType = props => <StarBorder {...props} />;
 +export const AdminMenuIcon: IconType = props => <Build {...props} />;
 +export const AdvancedIcon: IconType = props => <SettingsApplications {...props} />;
 +export const AttributesIcon: IconType = props => <ListAlt {...props} />;
 +export const BackIcon: IconType = props => <ArrowBack {...props} />;
 +export const CustomizeTableIcon: IconType = props => <Menu {...props} />;
 +export const CommandIcon: IconType = props => <LastPage {...props} />;
 +export const CopyIcon: IconType = props => <ContentCopy {...props} />;
 +export const FileCopyIcon: IconType = props => <FileCopy {...props} />;
 +export const CollectionIcon: IconType = props => <LibraryBooks {...props} />;
 +export const CloseIcon: IconType = props => <Close {...props} />;
 +export const CloudUploadIcon: IconType = props => <CloudUpload {...props} />;
 +export const DefaultIcon: IconType = props => <RateReview {...props} />;
 +export const DetailsIcon: IconType = props => <Info {...props} />;
 +export const DirectoryIcon: IconType = props => <Folder {...props} />;
 +export const DownloadIcon: IconType = props => <GetApp {...props} />;
 +export const EditSavedQueryIcon: IconType = props => <Create {...props} />;
 +export const ExpandIcon: IconType = props => <ExpandMoreIcon {...props} />;
 +export const ErrorIcon: IconType = props => (
 +    <ErrorRoundedIcon
 +        style={{ color: "#ff0000" }}
 +        {...props}
 +    />
 +);
 +export const FavoriteIcon: IconType = props => <Star {...props} />;
 +export const FileIcon: IconType = props => <DescriptionIcon {...props} />;
 +export const HelpIcon: IconType = props => <Help {...props} />;
 +export const HelpOutlineIcon: IconType = props => <HelpOutline {...props} />;
 +export const ImportContactsIcon: IconType = props => <ImportContacts {...props} />;
 +export const InfoIcon: IconType = props => <Info {...props} />;
 +export const FileInputIcon: IconType = props => <InsertDriveFile {...props} />;
 +export const KeyIcon: IconType = props => <VpnKey {...props} />;
 +export const LogIcon: IconType = props => <SettingsEthernet {...props} />;
 +export const MailIcon: IconType = props => <Mail {...props} />;
 +export const MaximizeIcon: IconType = props => <FullscreenSharp {...props} />;
 +export const ResourceIcon: IconType = props => <Memory {...props} />;
 +export const UnMaximizeIcon: IconType = props => <FullscreenExitSharp {...props} />;
 +export const MoreVerticalIcon: IconType = props => <MoreVert {...props} />;
 +export const MoreHorizontalIcon: IconType = props => <MoreHoriz {...props} />;
 +export const MoveToIcon: IconType = props => <Input {...props} />;
 +export const NewProjectIcon: IconType = props => <CreateNewFolder {...props} />;
 +export const NotificationIcon: IconType = props => <Notifications {...props} />;
 +export const OpenIcon: IconType = props => <OpenInNew {...props} />;
 +export const InputIcon: IconType = props => <MoveToInbox {...props} />;
 +export const PaginationDownIcon: IconType = props => <ArrowDropDown {...props} />;
 +export const PaginationLeftArrowIcon: IconType = props => <ChevronLeft {...props} />;
 +export const PaginationRightArrowIcon: IconType = props => <ChevronRight {...props} />;
 +export const ProcessIcon: IconType = props => <Settings {...props} />;
 +export const ProjectIcon: IconType = props => <Folder {...props} />;
 +export const FilterGroupIcon: IconType = props => <Pageview {...props} />;
 +export const ProjectsIcon: IconType = props => <Inbox {...props} />;
 +export const ProvenanceGraphIcon: IconType = props => <DeviceHub {...props} />;
 +export const RemoveIcon: IconType = props => <Delete {...props} />;
 +export const RemoveFavoriteIcon: IconType = props => <Star {...props} />;
 +export const PublicFavoriteIcon: IconType = props => <Public {...props} />;
 +export const RenameIcon: IconType = props => <Edit {...props} />;
 +export const RestoreVersionIcon: IconType = props => <FlipToFront {...props} />;
 +export const RestoreFromTrashIcon: IconType = props => <RestoreFromTrash {...props} />;
 +export const ReRunProcessIcon: IconType = props => <Cached {...props} />;
 +export const SearchIcon: IconType = props => <Search {...props} />;
 +export const ShareIcon: IconType = props => <PersonAdd {...props} />;
 +export const ShareMeIcon: IconType = props => <People {...props} />;
 +export const SidePanelRightArrowIcon: IconType = props => <PlayArrow {...props} />;
 +export const TrashIcon: IconType = props => <Delete {...props} />;
 +export const UserPanelIcon: IconType = props => <Person {...props} />;
 +export const UsedByIcon: IconType = props => <Folder {...props} />;
 +export const WorkflowIcon: IconType = props => <Code {...props} />;
 +export const WarningIcon: IconType = props => (
 +    <Warning
 +        style={{ color: "#fbc02d", height: "30px", width: "30px" }}
 +        {...props}
 +    />
 +);
 +export const Link: IconType = props => <LinkOutlined {...props} />;
 +export const FolderSharedIcon: IconType = props => <FolderShared {...props} />;
 +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 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} />;
 +export const LoginAsIcon: IconType = props => <ExitToApp {...props} />;
 +export const ActiveIcon: IconType = props => <CheckCircleOutline {...props} />;
 +export const SetupIcon: IconType = props => <RemoveCircleOutline {...props} />;
 +export const InactiveIcon: IconType = props => <NotInterested {...props} />;
 +export const ImageIcon: IconType = props => <Image {...props} />;
 +export const StartIcon: IconType = props => <PlayArrow {...props} />;
 +export const StopIcon: IconType = props => <Stop {...props} />;
 +export const SelectAllIcon: IconType = props => <CheckboxMultipleOutline {...props} />;
 +export const SelectNoneIcon: IconType = props => <CheckboxMultipleBlankOutline {...props} />;
index 4eff8885fcd5e6e7e034eab672588c340bf92f22,f92c0dcf4eb6147bd0d645532ca55c29e30d73cd..f92c0dcf4eb6147bd0d645532ca55c29e30d73cd
@@@ -2,7 -2,7 +2,7 @@@
  //
  // SPDX-License-Identifier: AGPL-3.0
  
- import React from "react";
+ import React, { useEffect, useState } from "react";
  import { connect } from "react-redux";
  import { StyleRulesCallback, withStyles, WithStyles, Toolbar, Tooltip, IconButton } from "@material-ui/core";
  import { ArvadosTheme } from "common/custom-theme";
@@@ -10,94 -10,173 +10,173 @@@ import { RootState } from "store/store"
  import { Dispatch } from "redux";
  import { TCheckedList } from "components/data-table/data-table";
  import { ContextMenuResource } from "store/context-menu/context-menu-actions";
- import { Resource, extractUuidKind } from "models/resource";
+ import { Resource, ResourceKind, extractUuidKind } from "models/resource";
  import { getResource } from "store/resources/resources";
  import { ResourcesState } from "store/resources/resources";
- import { ContextMenuAction, ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
- import { RestoreFromTrashIcon, TrashIcon } from "components/icon/icon";
- import { multiselectActionsFilters, TMultiselectActionsFilters, contextMenuActionConsts } from "./ms-toolbar-action-filters";
+ import { MultiSelectMenuAction, MultiSelectMenuActionSet } from "views-components/multiselect-toolbar/ms-menu-actions";
+ import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+ import { ContextMenuAction } from "views-components/context-menu/context-menu-action-set";
+ import { multiselectActionsFilters, TMultiselectActionsFilters, msMenuResourceKind } from "./ms-toolbar-action-filters";
  import { kindToActionSet, findActionByName } from "./ms-kind-action-differentiator";
  import { msToggleTrashAction } from "views-components/multiselect-toolbar/ms-project-action-set";
  import { copyToClipboardAction } from "store/open-in-new-tab/open-in-new-tab.actions";
  import { ContainerRequestResource } from "models/container-request";
+ import { FavoritesState } from "store/favorites/favorites-reducer";
+ import { resourceIsFrozen } from "common/frozen-resources";
+ import { getResourceWithEditableStatus } from "store/resources/resources";
+ import { GroupResource } from "models/group";
+ import { EditableResource } from "models/resource";
+ import { User } from "models/user";
+ import { GroupClass } from "models/group";
+ import { isProcessCancelable } from "store/processes/process";
+ import { CollectionResource } from "models/collection";
+ import { getProcess } from "store/processes/process";
+ import { Process } from "store/processes/process";
+ import { PublicFavoritesState } from "store/public-favorites/public-favorites-reducer";
+ import { isExactlyOneSelected } from "store/multiselect/multiselect-actions";
  
- type CssRules = "root" | "button";
+ const WIDTH_TRANSITION = 150
+ type CssRules = "root" | "transition" | "button" | "iconContainer";
  
  const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
      root: {
          display: "flex",
-         flexShrink: 0,
          flexDirection: "row",
          width: 0,
+         height: '2.7rem',
          padding: 0,
          margin: "1rem auto auto 0.5rem",
-         overflow: "hidden",
-         transition: "width 150ms",
+         transition: `width ${WIDTH_TRANSITION}ms`,
+         overflowY: 'auto',
+         scrollBehavior: 'smooth',
+         '&::-webkit-scrollbar': {
+             width: 0,
+             height: 2
+         },
+         '&::-webkit-scrollbar-track': {
+             width: 0,
+             height: 2
+         },
+         '&::-webkit-scrollbar-thumb': {
+             backgroundColor: '#757575',
+             borderRadius: 2
+         }
+     },
+     transition: {
+         display: "flex",
+         flexDirection: "row",
+         width: 0,
+         height: '2.7rem',
+         padding: 0,
+         margin: "1rem auto auto 0.5rem",
+         overflow: 'hidden',
+         transition: `width ${WIDTH_TRANSITION}ms`,
      },
      button: {
          width: "2.5rem",
          height: "2.5rem ",
      },
+     iconContainer: {
+         height: '100%'
+     }
  });
  
  export type MultiselectToolbarProps = {
      checkedList: TCheckedList;
-     resources: ResourcesState;
+     singleSelectedUuid: string | null
+     iconProps: IconProps
+     user: User | null
+     disabledButtons: Set<string>
      executeMulti: (action: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState) => void;
  };
  
+ type IconProps = {
+     resources: ResourcesState;
+     favorites: FavoritesState;
+     publicFavorites: PublicFavoritesState;
+ }
  export const MultiselectToolbar = connect(
      mapStateToProps,
      mapDispatchToProps
  )(
      withStyles(styles)((props: MultiselectToolbarProps & WithStyles<CssRules>) => {
-         const { classes, checkedList } = props;
-         const currentResourceKinds = Array.from(selectedToKindSet(checkedList));
+         const { classes, checkedList, singleSelectedUuid, iconProps, user, disabledButtons } = props;
+         const singleResourceKind = singleSelectedUuid ? [resourceToMsResourceKind(singleSelectedUuid, iconProps.resources, user)] : null
+         const currentResourceKinds = singleResourceKind ? singleResourceKind : Array.from(selectedToKindSet(checkedList));
          const currentPathIsTrash = window.location.pathname === "/trash";
-         const buttons =
+         const [isTransitioning, setIsTransitioning] = useState(false);
+         
+         const handleTransition = () => {
+             setIsTransitioning(true)
+             setTimeout(() => {
+                 setIsTransitioning(false)
+             }, WIDTH_TRANSITION);
+         }
+         
+         useEffect(()=>{
+                 handleTransition()
+         }, [checkedList])
+         const actions =
              currentPathIsTrash && selectedToKindSet(checkedList).size
                  ? [msToggleTrashAction]
-                 : selectActionsByKind(currentResourceKinds, multiselectActionsFilters);
+                 : selectActionsByKind(currentResourceKinds as string[], multiselectActionsFilters).filter((action) =>
+                         singleSelectedUuid === null ? action.isForMulti : true
+                     );
  
          return (
              <React.Fragment>
                  <Toolbar
-                     className={classes.root}
-                     style={{ width: `${buttons.length * 2.5}rem` }}
-                 >
-                     {buttons.length ? (
-                         buttons.map((btn, i) =>
-                             btn.name === "ToggleTrashAction" ? (
-                                 <Tooltip
-                                     className={classes.button}
-                                     title={currentPathIsTrash ? "Restore selected" : "Move to trash"}
-                                     key={i}
-                                     disableFocusListener
-                                 >
-                                     <IconButton onClick={() => props.executeMulti(btn, checkedList, props.resources)}>
-                                         {currentPathIsTrash ? <RestoreFromTrashIcon /> : <TrashIcon />}
+                     className={isTransitioning ? classes.transition: classes.root}
+                     style={{ width: `${(actions.length * 2.5) + 1}rem` }}
+                     data-cy='multiselect-toolbar'
+                     >
+                     {actions.length ? (
+                         actions.map((action, i) =>{
+                             const { hasAlts, useAlts, name, altName, icon, altIcon } = action;
+                         return hasAlts ? (
+                             <Tooltip
+                                 className={classes.button}
+                                 title={currentPathIsTrash || (useAlts && useAlts(singleSelectedUuid, iconProps)) ? altName : name}
+                                 key={i}
+                                 disableFocusListener
+                             >
+                                 <span className={classes.iconContainer}>
+                                     <IconButton
+                                         data-cy='multiselect-button'
+                                         disabled={disabledButtons.has(name)}
+                                         onClick={() => props.executeMulti(action, checkedList, iconProps.resources)}
+                                     >
+                                         {currentPathIsTrash || (useAlts && useAlts(singleSelectedUuid, iconProps)) ? altIcon && altIcon({}) : icon({})}
                                      </IconButton>
-                                 </Tooltip>
-                             ) : (
-                                 <Tooltip
-                                     className={classes.button}
-                                     title={btn.name}
-                                     key={i}
-                                     disableFocusListener
-                                 >
-                                     <IconButton onClick={() => props.executeMulti(btn, checkedList, props.resources)}>
-                                         {btn.icon ? btn.icon({}) : <></>}
+                                 </span>
+                             </Tooltip>
+                         ) : (
+                             <Tooltip
+                                 className={classes.button}
+                                 title={action.name}
+                                 key={i}
+                                 disableFocusListener
+                             >
+                                 <span className={classes.iconContainer}>
+                                     <IconButton
+                                         data-cy='multiselect-button'
+                                         onClick={() => props.executeMulti(action, checkedList, iconProps.resources)}
+                                     >
+                                         {action.icon({})}
                                      </IconButton>
-                                 </Tooltip>
-                             )
-                         )
+                                 </span>
+                             </Tooltip>
+                         );
+                         })
                      ) : (
                          <></>
                      )}
                  </Toolbar>
              </React.Fragment>
-         );
+         )
      })
  );
  
@@@ -131,14 -210,75 +210,75 @@@ function groupByKind(checkedList: TChec
      return result;
  }
  
- function filterActions(actionArray: ContextMenuActionSet, filters: Set<string>): Array<ContextMenuAction> {
+ function filterActions(actionArray: MultiSelectMenuActionSet, filters: Set<string>): Array<MultiSelectMenuAction> {
      return actionArray[0].filter(action => filters.has(action.name as string));
  }
  
+ const resourceToMsResourceKind = (uuid: string, resources: ResourcesState, user: User | null, readonly = false): (msMenuResourceKind | ResourceKind) | undefined => {
+     if (!user) return;
+     const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, user.uuid)(resources);
+     const { isAdmin } = user;
+     const kind = extractUuidKind(uuid);
+     const isFrozen = resourceIsFrozen(resource, resources);
+     const isEditable = (user.isAdmin || (resource || ({} as EditableResource)).isEditable) && !readonly && !isFrozen;
+     switch (kind) {
+         case ResourceKind.PROJECT:
+             if (isFrozen) {
+                 return isAdmin ? msMenuResourceKind.FROZEN_PROJECT_ADMIN : msMenuResourceKind.FROZEN_PROJECT;
+             }
+             return isAdmin && !readonly
+                 ? resource && resource.groupClass !== GroupClass.FILTER
+                     ? msMenuResourceKind.PROJECT_ADMIN
+                     : msMenuResourceKind.FILTER_GROUP_ADMIN
+                 : isEditable
+                 ? resource && resource.groupClass !== GroupClass.FILTER
+                     ? msMenuResourceKind.PROJECT
+                     : msMenuResourceKind.FILTER_GROUP
+                 : msMenuResourceKind.READONLY_PROJECT;
+         case ResourceKind.COLLECTION:
+             const c = getResource<CollectionResource>(uuid)(resources);
+             if (c === undefined) {
+                 return;
+             }
+             const isOldVersion = c.uuid !== c.currentVersionUuid;
+             const isTrashed = c.isTrashed;
+             return isOldVersion
+                 ? msMenuResourceKind.OLD_VERSION_COLLECTION
+                 : isTrashed && isEditable
+                 ? msMenuResourceKind.TRASHED_COLLECTION
+                 : isAdmin && isEditable
+                 ? msMenuResourceKind.COLLECTION_ADMIN
+                 : isEditable
+                 ? msMenuResourceKind.COLLECTION
+                 : msMenuResourceKind.READONLY_COLLECTION;
+         case ResourceKind.PROCESS:
+             return isAdmin && isEditable
+                 ? resource && isProcessCancelable(getProcess(resource.uuid)(resources) as Process)
+                     ? msMenuResourceKind.RUNNING_PROCESS_ADMIN
+                     : msMenuResourceKind.PROCESS_ADMIN
+                 : readonly
+                 ? msMenuResourceKind.READONLY_PROCESS_RESOURCE
+                 : resource && isProcessCancelable(getProcess(resource.uuid)(resources) as Process)
+                 ? msMenuResourceKind.RUNNING_PROCESS_RESOURCE
+                 : msMenuResourceKind.PROCESS_RESOURCE;
+         case ResourceKind.USER:
+             return msMenuResourceKind.ROOT_PROJECT;
+         case ResourceKind.LINK:
+             return msMenuResourceKind.LINK;
+         case ResourceKind.WORKFLOW:
+             return isEditable ? msMenuResourceKind.WORKFLOW : msMenuResourceKind.READONLY_WORKFLOW;
+         default:
+             return;
+     }
+ }; 
  function selectActionsByKind(currentResourceKinds: Array<string>, filterSet: TMultiselectActionsFilters) {
-     const rawResult: Set<ContextMenuAction> = new Set();
+     const rawResult: Set<MultiSelectMenuAction> = new Set();
      const resultNames = new Set();
-     const allFiltersArray: ContextMenuAction[][] = [];
+     const allFiltersArray: MultiSelectMenuAction[][] = []
      currentResourceKinds.forEach(kind => {
          if (filterSet[kind]) {
              const actions = filterActions(...filterSet[kind]);
      });
  
      const filteredNameSet = allFiltersArray.map(filterArray => {
-         const resultSet = new Set();
-         filterArray.forEach(action => resultSet.add(action.name || ""));
+         const resultSet = new Set<string>();
+         filterArray.forEach(action => resultSet.add(action.name as string || ""));
          return resultSet;
      });
  
      const filteredResult = Array.from(rawResult).filter(action => {
          for (let i = 0; i < filteredNameSet.length; i++) {
-             if (!filteredNameSet[i].has(action.name)) return false;
+             if (!filteredNameSet[i].has(action.name as string)) return false;
          }
          return true;
      });
      });
  }
  
  //--------------------------------------------------//
  
- function mapStateToProps(state: RootState) {
+ function mapStateToProps({auth, multiselect, resources, favorites, publicFavorites}: RootState) {
      return {
-         checkedList: state.multiselect.checkedList as TCheckedList,
-         resources: state.resources,
-     };
+         checkedList: multiselect.checkedList as TCheckedList,
+         singleSelectedUuid: isExactlyOneSelected(multiselect.checkedList),
+         user: auth && auth.user ? auth.user : null,
+         disabledButtons: new Set<string>(multiselect.disabledButtons),
+         iconProps: {
+             resources,
+             favorites,
+             publicFavorites
+         }
+     }
  }
  
  function mapDispatchToProps(dispatch: Dispatch) {
          executeMulti: (selectedAction: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState): void => {
              const kindGroups = groupByKind(checkedList, resources);
              switch (selectedAction.name) {
-                 case contextMenuActionConsts.MOVE_TO:
-                 case contextMenuActionConsts.REMOVE:
+                 case MultiSelectMenuActionNames.MOVE_TO:
+                 case MultiSelectMenuActionNames.REMOVE:
                      const firstResource = getResource(selectedToArray(checkedList)[0])(resources) as ContainerRequestResource | Resource;
                      const action = findActionByName(selectedAction.name as string, kindToActionSet[firstResource.kind]);
                      if (action) action.execute(dispatch, kindGroups[firstResource.kind]);
                      break;
-                 case contextMenuActionConsts.COPY_TO_CLIPBOARD:
+                 case MultiSelectMenuActionNames.COPY_TO_CLIPBOARD:
                      const selectedResources = selectedToArray(checkedList).map(uuid => getResource(uuid)(resources));
                      dispatch<any>(copyToClipboardAction(selectedResources));
                      break;
index e2f643b6870e76d8f9e56b4e77ce49e679f29f80,5a84d4c573f711a46a4a4665b9acbcff5bf2f18f..5a84d4c573f711a46a4a4665b9acbcff5bf2f18f
@@@ -3,19 -3,21 +3,21 @@@
  // SPDX-License-Identifier: AGPL-3.0
  
  import { ResourceKind } from "models/resource";
- import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
+ import { MultiSelectMenuActionSet} from "views-components/multiselect-toolbar/ms-menu-actions";
  import { msCollectionActionSet } from "views-components/multiselect-toolbar/ms-collection-action-set";
  import { msProjectActionSet } from "views-components/multiselect-toolbar/ms-project-action-set";
  import { msProcessActionSet } from "views-components/multiselect-toolbar/ms-process-action-set";
+ import { msWorkflowActionSet } from "views-components/multiselect-toolbar/ms-workflow-action-set";
  
- export function findActionByName(name: string, actionSet: ContextMenuActionSet) {
+ export function findActionByName(name: string, actionSet: MultiSelectMenuActionSet) {
      return actionSet[0].find(action => action.name === name);
  }
  
- const { COLLECTION, PROJECT, PROCESS } = ResourceKind;
+ const { COLLECTION, PROCESS, PROJECT, WORKFLOW } = ResourceKind;
  
- export const kindToActionSet: Record<string, ContextMenuActionSet> = {
+ export const kindToActionSet: Record<string, MultiSelectMenuActionSet> = {
      [COLLECTION]: msCollectionActionSet,
-     [PROJECT]: msProjectActionSet,
      [PROCESS]: msProcessActionSet,
+     [PROJECT]: msProjectActionSet,
+     [WORKFLOW]: msWorkflowActionSet,
  };
index 9145a820694c7196c2676209e47cff43e327ae2d,b34cc22cb9ab05934e4ff6568527091ee9dce45a..b34cc22cb9ab05934e4ff6568527091ee9dce45a
  //
  // SPDX-License-Identifier: AGPL-3.0
  
- import { ResourceKind } from "models/resource";
- import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
- import { msCollectionActionSet } from "views-components/multiselect-toolbar/ms-collection-action-set";
- import { msProjectActionSet } from "views-components/multiselect-toolbar/ms-project-action-set";
- import { msProcessActionSet } from "views-components/multiselect-toolbar/ms-process-action-set";
+ import { MultiSelectMenuActionSet } from 'views-components/multiselect-toolbar/ms-menu-actions';
+ import { msCollectionActionSet, msCommonCollectionActionFilter, msReadOnlyCollectionActionFilter } from 'views-components/multiselect-toolbar/ms-collection-action-set';
+ import {
+     msProjectActionSet,
+     msCommonProjectActionFilter,
+     msReadOnlyProjectActionFilter,
+     msFilterGroupActionFilter,
+     msAdminFilterGroupActionFilter,
+     msFrozenProjectActionFilter,
+     msAdminFrozenProjectActionFilter
+ } from 'views-components/multiselect-toolbar/ms-project-action-set';
+ import { msProcessActionSet, msCommonProcessActionFilter, msAdminProcessActionFilter, msRunningProcessActionFilter } from 'views-components/multiselect-toolbar/ms-process-action-set';
+ import { msWorkflowActionSet, msWorkflowActionFilter, msReadOnlyWorkflowActionFilter } from 'views-components/multiselect-toolbar/ms-workflow-action-set';
+ import { ResourceKind } from 'models/resource';
  
- export type TMultiselectActionsFilters = Record<string, [ContextMenuActionSet, Set<string>]>;
+ export enum msMenuResourceKind {
+     API_CLIENT_AUTHORIZATION = 'ApiClientAuthorization',
+     ROOT_PROJECT = 'RootProject',
+     PROJECT = 'Project',
+     FILTER_GROUP = 'FilterGroup',
+     READONLY_PROJECT = 'ReadOnlyProject',
+     FROZEN_PROJECT = 'FrozenProject',
+     FROZEN_PROJECT_ADMIN = 'FrozenProjectAdmin',
+     PROJECT_ADMIN = 'ProjectAdmin',
+     FILTER_GROUP_ADMIN = 'FilterGroupAdmin',
+     RESOURCE = 'Resource',
+     FAVORITE = 'Favorite',
+     TRASH = 'Trash',
+     COLLECTION_FILES = 'CollectionFiles',
+     COLLECTION_FILES_MULTIPLE = 'CollectionFilesMultiple',
+     READONLY_COLLECTION_FILES = 'ReadOnlyCollectionFiles',
+     READONLY_COLLECTION_FILES_MULTIPLE = 'ReadOnlyCollectionFilesMultiple',
+     COLLECTION_FILES_NOT_SELECTED = 'CollectionFilesNotSelected',
+     COLLECTION_FILE_ITEM = 'CollectionFileItem',
+     COLLECTION_DIRECTORY_ITEM = 'CollectionDirectoryItem',
+     READONLY_COLLECTION_FILE_ITEM = 'ReadOnlyCollectionFileItem',
+     READONLY_COLLECTION_DIRECTORY_ITEM = 'ReadOnlyCollectionDirectoryItem',
+     COLLECTION = 'Collection',
+     COLLECTION_ADMIN = 'CollectionAdmin',
+     READONLY_COLLECTION = 'ReadOnlyCollection',
+     OLD_VERSION_COLLECTION = 'OldVersionCollection',
+     TRASHED_COLLECTION = 'TrashedCollection',
+     PROCESS = 'Process',
+     RUNNING_PROCESS_ADMIN = 'RunningProcessAdmin',
+     PROCESS_ADMIN = 'ProcessAdmin',
+     RUNNING_PROCESS_RESOURCE = 'RunningProcessResource',
+     PROCESS_RESOURCE = 'ProcessResource',
+     READONLY_PROCESS_RESOURCE = 'ReadOnlyProcessResource',
+     PROCESS_LOGS = 'ProcessLogs',
+     REPOSITORY = 'Repository',
+     SSH_KEY = 'SshKey',
+     VIRTUAL_MACHINE = 'VirtualMachine',
+     KEEP_SERVICE = 'KeepService',
+     USER = 'User',
+     GROUPS = 'Group',
+     GROUP_MEMBER = 'GroupMember',
+     PERMISSION_EDIT = 'PermissionEdit',
+     LINK = 'Link',
+     WORKFLOW = 'Workflow',
+     READONLY_WORKFLOW = 'ReadOnlyWorkflow',
+     SEARCH_RESULTS = 'SearchResults',
+ }
  
- export const contextMenuActionConsts = {
-     MAKE_A_COPY: "Make a copy",
-     MOVE_TO: "Move to",
-     TOGGLE_TRASH_ACTION: "ToggleTrashAction",
-     COPY_TO_CLIPBOARD: "Copy to clipboard",
-     COPY_AND_RERUN_PROCESS: "Copy and re-run process",
-     REMOVE: "Remove",
- };
- const { MOVE_TO, TOGGLE_TRASH_ACTION, REMOVE, MAKE_A_COPY } = contextMenuActionConsts;
+ const {
+     COLLECTION,
+     COLLECTION_ADMIN,
+     READONLY_COLLECTION,
+     PROCESS_RESOURCE,
+     RUNNING_PROCESS_RESOURCE,
+     RUNNING_PROCESS_ADMIN,
+     PROCESS_ADMIN,
+     PROJECT,
+     PROJECT_ADMIN,
+     FROZEN_PROJECT,
+     FROZEN_PROJECT_ADMIN,
+     READONLY_PROJECT,
+     FILTER_GROUP,
+     FILTER_GROUP_ADMIN,
+     WORKFLOW,
+     READONLY_WORKFLOW,
+ } = msMenuResourceKind;
  
- //these sets govern what actions are on the ms toolbar for each resource kind
- const projectMSActionsFilter = new Set([MOVE_TO, TOGGLE_TRASH_ACTION]);
- const processResourceMSActionsFilter = new Set([MOVE_TO, REMOVE]);
- const collectionMSActionsFilter = new Set([MAKE_A_COPY, MOVE_TO, TOGGLE_TRASH_ACTION]);
+ export type TMultiselectActionsFilters = Record<string, [MultiSelectMenuActionSet, Set<string>]>;
  
- const { COLLECTION, PROJECT, PROCESS } = ResourceKind;
+ const allActionNames = (actionSet: MultiSelectMenuActionSet): Set<string> => new Set(actionSet[0].map((action) => action.name));
  
  export const multiselectActionsFilters: TMultiselectActionsFilters = {
-     [PROJECT]: [msProjectActionSet, projectMSActionsFilter],
-     [PROCESS]: [msProcessActionSet, processResourceMSActionsFilter],
-     [COLLECTION]: [msCollectionActionSet, collectionMSActionsFilter],
+     [COLLECTION]: [msCollectionActionSet, msCommonCollectionActionFilter],
+     [COLLECTION_ADMIN]: [msCollectionActionSet, allActionNames(msCollectionActionSet)],
+     [READONLY_COLLECTION]: [msCollectionActionSet, msReadOnlyCollectionActionFilter],
+     [ResourceKind.COLLECTION]: [msCollectionActionSet, msCommonCollectionActionFilter],
+     [PROCESS_RESOURCE]: [msProcessActionSet, msCommonProcessActionFilter],
+     [PROCESS_ADMIN]: [msProcessActionSet, msAdminProcessActionFilter],
+     [RUNNING_PROCESS_RESOURCE]: [msProcessActionSet, msRunningProcessActionFilter],
+     [RUNNING_PROCESS_ADMIN]: [msProcessActionSet, allActionNames(msProcessActionSet)],
+     [ResourceKind.PROCESS]: [msProcessActionSet, msCommonProcessActionFilter],
+     
+     [PROJECT]: [msProjectActionSet, msCommonProjectActionFilter],
+     [PROJECT_ADMIN]: [msProjectActionSet, allActionNames(msProjectActionSet)],
+     [FROZEN_PROJECT]: [msProjectActionSet, msFrozenProjectActionFilter],
+     [FROZEN_PROJECT_ADMIN]: [msProjectActionSet, msAdminFrozenProjectActionFilter], 
+     [READONLY_PROJECT]: [msProjectActionSet, msReadOnlyProjectActionFilter],
+     [ResourceKind.PROJECT]: [msProjectActionSet, msCommonProjectActionFilter],
+     
+     [FILTER_GROUP]: [msProjectActionSet, msFilterGroupActionFilter],
+     [FILTER_GROUP_ADMIN]: [msProjectActionSet, msAdminFilterGroupActionFilter],
+     
+     [WORKFLOW]: [msWorkflowActionSet, msWorkflowActionFilter],
+     [READONLY_WORKFLOW]: [msWorkflowActionSet, msReadOnlyWorkflowActionFilter],
  };
index 6107c40972d690df30ce36a02ed589f24d67fc13,772def29d05e9610ec53561234e8de523af3a18d..772def29d05e9610ec53561234e8de523af3a18d
@@@ -2,12 -2,18 +2,18 @@@
  //
  // SPDX-License-Identifier: AGPL-3.0
  
+ import { ofType, unionize } from 'common/unionize';
  import { Dispatch } from "redux";
  import { RootState } from "store/store";
  import { ServiceRepository } from "services/services";
  import { dialogActions } from 'store/dialog/dialog-actions';
- import { getNewExtraToken } from "../auth/auth-action";
  import { CollectionResource } from "models/collection";
+ import { SshKeyResource } from 'models/ssh-key';
+ import { User } from "models/user";
+ import { Session } from "models/session";
+ import { Config } from 'common/config';
+ import { createServices, setAuthorizationHeader } from "services/services";
+ import { getTokenV2 } from 'models/api-client-authorization';
  
  export const COLLECTION_WEBDAV_S3_DIALOG_NAME = 'collectionWebdavS3Dialog';
  
@@@ -42,3 -48,77 +48,77 @@@ export const openWebDavS3InfoDialog = (
              }
          }));
      };
+ const authActions = unionize({
+     LOGIN: {},
+     LOGOUT: ofType<{ deleteLinkData: boolean, preservePath: boolean }>(),
+     SET_CONFIG: ofType<{ config: Config }>(),
+     SET_EXTRA_TOKEN: ofType<{ extraApiToken: string, extraApiTokenExpiration?: Date }>(),
+     RESET_EXTRA_TOKEN: {},
+     INIT_USER: ofType<{ user: User, token: string, tokenExpiration?: Date, tokenLocation?: string }>(),
+     USER_DETAILS_REQUEST: {},
+     USER_DETAILS_SUCCESS: ofType<User>(),
+     SET_SSH_KEYS: ofType<SshKeyResource[]>(),
+     ADD_SSH_KEY: ofType<SshKeyResource>(),
+     REMOVE_SSH_KEY: ofType<string>(),
+     SET_HOME_CLUSTER: ofType<string>(),
+     SET_SESSIONS: ofType<Session[]>(),
+     ADD_SESSION: ofType<Session>(),
+     REMOVE_SESSION: ofType<string>(),
+     UPDATE_SESSION: ofType<Session>(),
+     REMOTE_CLUSTER_CONFIG: ofType<{ config: Config }>(),
+ });
+ const getConfig = (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Config => {
+     const state = getState().auth;
+     return state.remoteHostsConfig[state.localCluster];
+ };
+ const getNewExtraToken =
+     (reuseStored: boolean = false) =>
+     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+         const extraToken = getState().auth.extraApiToken;
+         if (reuseStored && extraToken !== undefined) {
+             const config = dispatch<any>(getConfig);
+             const svc = createServices(config, { progressFn: () => {}, errorFn: () => {} });
+             setAuthorizationHeader(svc, extraToken);
+             try {
+                 // Check the extra token's validity before using it. Refresh its
+                 // expiration date just in case it changed.
+                 const client = await svc.apiClientAuthorizationService.get('current');
+                 dispatch(
+                     authActions.SET_EXTRA_TOKEN({
+                         extraApiToken: extraToken,
+                         extraApiTokenExpiration: client.expiresAt ? new Date(client.expiresAt) : undefined,
+                     })
+                 );
+                 return extraToken;
+             } catch (e) {
+                 dispatch(authActions.RESET_EXTRA_TOKEN());
+             }
+         }
+         const user = getState().auth.user;
+         const loginCluster = getState().auth.config.clusterConfig.Login.LoginCluster;
+         if (user === undefined) {
+             return;
+         }
+         if (loginCluster !== '' && getState().auth.homeCluster !== loginCluster) {
+             return;
+         }
+         try {
+             // Do not show errors on the create call, cluster security configuration may not
+             // allow token creation and there's no way to know that from workbench2 side in advance.
+             const client = await services.apiClientAuthorizationService.create(undefined, false);
+             const newExtraToken = getTokenV2(client);
+             dispatch(
+                 authActions.SET_EXTRA_TOKEN({
+                     extraApiToken: newExtraToken,
+                     extraApiTokenExpiration: client.expiresAt ? new Date(client.expiresAt) : undefined,
+                 })
+             );
+             return newExtraToken;
+         } catch {
+             console.warn("Cannot create new tokens with the current token, probably because of cluster's security settings.");
+             return;
+         }
+     };
index 1e23f35cbfd8b4d1beced3aae5ee043c28875a53,da454ed77dbc9561116cf43c6de5fc25ae8edc95..da454ed77dbc9561116cf43c6de5fc25ae8edc95
@@@ -10,6 -10,8 +10,8 @@@ import { checkFavorite } from "./favori
  import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
  import { ServiceRepository } from "services/services";
  import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
+ import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+ import { addDisabledButton, removeDisabledButton } from "store/multiselect/multiselect-actions";
  import { loadFavoritesTree} from "store/side-panel-tree/side-panel-tree-actions";
  
  export const favoritesActions = unionize({
@@@ -27,6 -29,7 +29,7 @@@ export const toggleFavorite = (resource
              return Promise.reject("No user");
          }
          dispatch(progressIndicatorActions.START_WORKING("toggleFavorite"));
+         dispatch<any>(addDisabledButton(MultiSelectMenuActionNames.ADD_TO_FAVORITES))
          dispatch(favoritesActions.TOGGLE_FAVORITE({ resourceUuid: resource.uuid }));
          const isFavorite = checkFavorite(resource.uuid, getState().favorites);
          dispatch(snackbarActions.OPEN_SNACKBAR({
@@@ -51,6 -54,7 +54,7 @@@
                      hideDuration: 2000,
                      kind: SnackbarKind.SUCCESS
                  }));
+                 dispatch<any>(removeDisabledButton(MultiSelectMenuActionNames.ADD_TO_FAVORITES))
                  dispatch(progressIndicatorActions.STOP_WORKING("toggleFavorite"));
                  dispatch<any>(loadFavoritesTree())
              })
index 1c329a9e90496bb87b168e55b8302c9808f6c555,a246ddbcc02a597c05e293a7b2c79f5e2d04e641..a246ddbcc02a597c05e293a7b2c79f5e2d04e641
@@@ -3,11 -3,45 +3,45 @@@
  // SPDX-License-Identifier: AGPL-3.0
  
  import { TCheckedList } from "components/data-table/data-table";
+ import { ContainerRequestResource } from "models/container-request";
+ import { Dispatch } from "redux";
+ import { navigateTo } from "store/navigation/navigation-action";
+ import { snackbarActions } from "store/snackbar/snackbar-actions";
+ import { RootState } from "store/store";
+ import { ServiceRepository } from "services/services";
+ import { SnackbarKind } from "store/snackbar/snackbar-actions";
+ import { ContextMenuResource } from 'store/context-menu/context-menu-actions';
  
  export const multiselectActionContants = {
      TOGGLE_VISIBLITY: "TOGGLE_VISIBLITY",
      SET_CHECKEDLIST: "SET_CHECKEDLIST",
+     SELECT_ONE: 'SELECT_ONE',
      DESELECT_ONE: "DESELECT_ONE",
+     TOGGLE_ONE: 'TOGGLE_ONE',
+     SET_SELECTED_UUID: 'SET_SELECTED_UUID',
+     ADD_DISABLED: 'ADD_DISABLED',
+     REMOVE_DISABLED: 'REMOVE_DISABLED',
+ };
+ export const msNavigateToOutput = (resource: ContextMenuResource | ContainerRequestResource) => async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+     try {
+         await services.collectionService.get(resource.outputUuid || '');
+         dispatch<any>(navigateTo(resource.outputUuid || ''));
+     } catch {
+         dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Output collection was trashed or deleted.", hideDuration: 4000, kind: SnackbarKind.WARNING }));
+     }
+ };
+ export const isExactlyOneSelected = (checkedList: TCheckedList) => {
+     let tally = 0;
+     let current = '';
+     for (const uuid in checkedList) {
+         if (checkedList[uuid] === true) {
+             tally++;
+             current = uuid;
+         }
+     }
+     return tally === 1 ? current : null
  };
  
  export const toggleMSToolbar = (isVisible: boolean) => {
  
  export const setCheckedListOnStore = (checkedList: TCheckedList) => {
      return dispatch => {
+         dispatch(setSelectedUuid(isExactlyOneSelected(checkedList)))
          dispatch({ type: multiselectActionContants.SET_CHECKEDLIST, payload: checkedList });
      };
  };
  
+ export const selectOne = (uuid: string) => {
+     return dispatch => {
+         dispatch({ type: multiselectActionContants.SELECT_ONE, payload: uuid });
+     };
+ };
  export const deselectOne = (uuid: string) => {
      return dispatch => {
          dispatch({ type: multiselectActionContants.DESELECT_ONE, payload: uuid });
      };
  };
  
+ export const toggleOne = (uuid: string) => {
+     return dispatch => {
+         dispatch({ type: multiselectActionContants.TOGGLE_ONE, payload: uuid });
+     };
+ };
+ export const setSelectedUuid = (uuid: string | null) => {
+     return dispatch => {
+         dispatch({ type: multiselectActionContants.SET_SELECTED_UUID, payload: uuid });
+     };
+ };
+ export const addDisabledButton = (buttonName: string) => {
+     return dispatch => {
+         dispatch({ type: multiselectActionContants.ADD_DISABLED, payload: buttonName });
+     };
+ };
+ export const removeDisabledButton = (buttonName: string) => {
+     return dispatch => {
+         dispatch({ type: multiselectActionContants.REMOVE_DISABLED, payload: buttonName });
+     };
+ };
  export const multiselectActions = {
      toggleMSToolbar,
      setCheckedListOnStore,
+     selectOne,
      deselectOne,
+     toggleOne,
+     setSelectedUuid,
+     addDisabledButton,
+     removeDisabledButton,
  };
index 75c4b1f99388d5567b98cf7955c59db297355fd1,26b853933c066abd2e1629af203a6a4327d7b6bc..26b853933c066abd2e1629af203a6a4327d7b6bc
@@@ -8,14 -8,18 +8,18 @@@ import { TCheckedList } from "component
  type MultiselectToolbarState = {
      isVisible: boolean;
      checkedList: TCheckedList;
+     selectedUuid: string;
+     disabledButtons: string[]
  };
  
  const multiselectToolbarInitialState = {
      isVisible: false,
      checkedList: {},
+     selectedUuid: '',
+     disabledButtons: []
  };
  
- const { TOGGLE_VISIBLITY, SET_CHECKEDLIST, DESELECT_ONE } = multiselectActionContants;
+ const { TOGGLE_VISIBLITY, SET_CHECKEDLIST, SELECT_ONE, DESELECT_ONE, TOGGLE_ONE, SET_SELECTED_UUID, ADD_DISABLED, REMOVE_DISABLED } = multiselectActionContants;
  
  export const multiselectReducer = (state: MultiselectToolbarState = multiselectToolbarInitialState, action) => {
      switch (action.type) {
              return { ...state, isVisible: action.payload };
          case SET_CHECKEDLIST:
              return { ...state, checkedList: action.payload };
+         case SELECT_ONE:
+             return { ...state, checkedList: { ...state.checkedList, [action.payload]: true } };
          case DESELECT_ONE:
              return { ...state, checkedList: { ...state.checkedList, [action.payload]: false } };
+         case TOGGLE_ONE:
+             return { ...state, checkedList: { ...state.checkedList, [action.payload]: !state.checkedList[action.payload] } };
+         case SET_SELECTED_UUID:
+             return {...state, selectedUuid: action.payload || ''}
+         case ADD_DISABLED:
+             return { ...state, disabledButtons: [...state.disabledButtons, action.payload]}
+         case REMOVE_DISABLED:
+             return { ...state, disabledButtons: state.disabledButtons.filter((button) => button !== action.payload) };
          default:
              return state;
      }
index 81f8dd6ba0dc456f980ffaec17bf25000c5afa3d,2111afdb2fc89d05eaba56ad46add0c43beccf19..2111afdb2fc89d05eaba56ad46add0c43beccf19
@@@ -20,6 -20,7 +20,7 @@@ import { CommandInputParameter, getIOPa
  import { getIOParamDisplayValue, ProcessIOParameter } from "views/process-panel/process-io-card";
  import { OutputDetails, NodeInstanceType, NodeInfo } from "./process-panel";
  import { AuthState } from "store/auth/auth-reducer";
+ import { ContextMenuResource } from "store/context-menu/context-menu-actions";
  
  export const processPanelActions = unionize({
      RESET_PROCESS_PANEL: ofType<{}>(),
@@@ -53,10 -54,10 +54,10 @@@ export const loadProcessPanel = (uuid: 
      dispatch<any>(loadSubprocessPanel());
  };
  
- export const navigateToOutput = (uuid: string) => async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ export const navigateToOutput = (resource: ContextMenuResource | ContainerRequestResource) => async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
      try {
-         await services.collectionService.get(uuid);
-         dispatch<any>(navigateTo(uuid));
+         await services.collectionService.get(resource.outputUuid || '');
+         dispatch<any>(navigateTo(resource.outputUuid || ''));
      } catch {
          dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Output collection was trashed or deleted.", hideDuration: 4000, kind: SnackbarKind.WARNING }));
      }
index b72058d56e81dd0ed4a42dce663357ffcaea4dd9,366e15ae04759e2a774563d8307fa8c4449eb8fe..366e15ae04759e2a774563d8307fa8c4449eb8fe
@@@ -35,6 -35,8 +35,8 @@@ import { updatePublicFavorites } from "
  import { selectedFieldsOfGroup } from "models/group";
  import { defaultCollectionSelectedFields } from "models/collection";
  import { containerRequestFieldsNoMounts } from "models/container-request";
+ import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+ import { removeDisabledButton } from "store/multiselect/multiselect-actions";
  
  export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
      constructor(private services: ServiceRepository, id: string) {
                      api.dispatch(couldNotFetchProjectContents());
                  }
              } finally {
-                 if (!background) { api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); }
+                 if (!background) { 
+                     api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+                     api.dispatch<any>(removeDisabledButton(MultiSelectMenuActionNames.MOVE_TO_TRASH))
+                 }
              }
          }
      }
index 84f886d6db6026a9f3ce3636d75044451d4567d0,28e934d1f85ff988f5d5fc73df6d2faadca3d450..28e934d1f85ff988f5d5fc73df6d2faadca3d450
@@@ -7,25 -7,31 +7,31 @@@ import { ServiceRepository } from "serv
  import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
  import { loadResource } from "store/resources/resources-actions";
  import { RootState } from "store/store";
+ import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+ import { addDisabledButton, removeDisabledButton } from "store/multiselect/multiselect-actions";
  
  export const freezeProject = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+     dispatch<any>(addDisabledButton(MultiSelectMenuActionNames.FREEZE_PROJECT))
      const userUUID = getState().auth.user!.uuid;
+     
      const updatedProject = await services.projectService.update(uuid, {
          frozenByUuid: userUUID,
      });
+     
      dispatch(projectPanelActions.REQUEST_ITEMS());
      dispatch<any>(loadResource(uuid, false));
+     dispatch<any>(removeDisabledButton(MultiSelectMenuActionNames.FREEZE_PROJECT))
      return updatedProject;
  };
  
  export const unfreezeProject = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+     dispatch<any>(addDisabledButton(MultiSelectMenuActionNames.FREEZE_PROJECT))
      const updatedProject = await services.projectService.update(uuid, {
          frozenByUuid: null,
      });
  
      dispatch(projectPanelActions.REQUEST_ITEMS());
      dispatch<any>(loadResource(uuid, false));
+     dispatch<any>(removeDisabledButton(MultiSelectMenuActionNames.FREEZE_PROJECT))
      return updatedProject;
  };
index 363b0b44a97018b6885f68ca1ac0d589ef410f52,0f8ed6c2611c72e55fc769a2d5f4b7ab3d4c6408..0f8ed6c2611c72e55fc769a2d5f4b7ab3d4c6408
@@@ -9,6 -9,8 +9,8 @@@ import { checkPublicFavorite } from "./
  import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
  import { ServiceRepository } from "services/services";
  import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
+ import { addDisabledButton, removeDisabledButton } from "store/multiselect/multiselect-actions";
+ import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
  import { loadPublicFavoritesTree } from "store/side-panel-tree/side-panel-tree-actions";
  
  export const publicFavoritesActions = unionize({
@@@ -22,6 -24,7 +24,7 @@@ export type PublicFavoritesAction = Uni
  export const togglePublicFavorite = (resource: { uuid: string; name: string }) =>
      (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
          dispatch(progressIndicatorActions.START_WORKING("togglePublicFavorite"));
+         dispatch<any>(addDisabledButton(MultiSelectMenuActionNames.ADD_TO_PUBLIC_FAVORITES))
          const uuidPrefix = getState().auth.config.uuidPrefix;
          const uuid = `${uuidPrefix}-j7d0g-publicfavorites`;
          dispatch(publicFavoritesActions.TOGGLE_PUBLIC_FAVORITE({ resourceUuid: resource.uuid }));
@@@ -48,6 -51,7 +51,7 @@@
                      hideDuration: 2000,
                      kind: SnackbarKind.SUCCESS
                  }));
+                 dispatch<any>(removeDisabledButton(MultiSelectMenuActionNames.ADD_TO_PUBLIC_FAVORITES))
                  dispatch(progressIndicatorActions.STOP_WORKING("togglePublicFavorite"));
                  dispatch<any>(loadPublicFavoritesTree())
              })
index d72b6ad7a1ab62ee59c83ce5e765ff6d5177de44,c822cece8742856330a7d7734cd655cae923aec7..c822cece8742856330a7d7734cd655cae923aec7
@@@ -27,7 -27,8 +27,8 @@@ import { serializeResourceTypeFilters 
  import { getDataExplorerColumnFilters } from 'store/data-explorer/data-explorer-middleware-service';
  import { joinFilters } from 'services/api/filter-builder';
  import { CollectionResource } from "models/collection";
+ import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+ import { removeDisabledButton } from "store/multiselect/multiselect-actions";
  export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
      constructor(private services: ServiceRepository, id: string) {
          super(id);
@@@ -84,6 -85,7 +85,7 @@@
              }));
              api.dispatch(couldNotFetchTrashContents());
          }
+         api.dispatch<any>(removeDisabledButton(MultiSelectMenuActionNames.MOVE_TO_TRASH))
      }
  }
  
index 62b669220e68e2e6d3089f878f7fe4e73f29b962,f4e3d3f0c4de225406cff2a8c4b6e1c9eed61fe9..f4e3d3f0c4de225406cff2a8c4b6e1c9eed61fe9
@@@ -13,6 -13,8 +13,8 @@@ import { sharedWithMePanelActions } fro
  import { ResourceKind } from "models/resource";
  import { navigateTo, navigateToTrash } from "store/navigation/navigation-action";
  import { matchCollectionRoute, matchSharedWithMeRoute } from "routes/routes";
+ import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+ import { addDisabledButton } from "store/multiselect/multiselect-actions";
  
  export const toggleProjectTrashed =
      (uuid: string, ownerUuid: string, isTrashed: boolean, isMulti: boolean) =>
              let errorMessage = "";
              let successMessage = "";
              let untrashedResource;
+             dispatch<any>(addDisabledButton(MultiSelectMenuActionNames.MOVE_TO_TRASH))
              try {
                  if (isTrashed) {
                      errorMessage = "Could not restore project from trash";
                      successMessage = "Restored project from trash";
-                     untrashedResource = await services.groupsService.untrash(uuid);
+                      untrashedResource = await services.groupsService.untrash(uuid);
                      dispatch<any>(isMulti || !untrashedResource ? navigateToTrash : navigateTo(uuid));
                      dispatch<any>(activateSidePanelTreeItem(uuid));
                  } else {
@@@ -32,7 -35,7 +35,7 @@@
                      successMessage = "Added project to trash";
                      await services.groupsService.trash(uuid);
                      dispatch<any>(loadSidePanelTreeProjects(ownerUuid));
+                     
                      const { location } = getState().router;
                      if (matchSharedWithMeRoute(location ? location.pathname : "")) {
                          dispatch(sharedWithMePanelActions.REQUEST_ITEMS());
@@@ -42,7 -45,7 +45,7 @@@
                      }
                  }
                  if (untrashedResource) {
-                     dispatch(
+                         dispatch(
                          snackbarActions.OPEN_SNACKBAR({
                              message: successMessage,
                              hideDuration: 2000,
@@@ -74,6 -77,7 +77,7 @@@ export const toggleCollectionTrashed 
          async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
              let errorMessage = "";
              let successMessage = "";
+             dispatch<any>(addDisabledButton(MultiSelectMenuActionNames.MOVE_TO_TRASH))
              try {
                  if (isTrashed) {
                      const { location } = getState().router;
index 7795e52d1a71be15bfc38c823945b7cb050667a8,0000000000000000000000000000000000000000..b286186aba5eb71a9803fe096e9022c7ecfbcba2
mode 100644,000000..100644
--- /dev/null
@@@ -1,880 -1,0 +1,880 @@@
-                             message: e.message,
 +// Copyright (C) The Arvados Authors. All rights reserved.
 +//
 +// SPDX-License-Identifier: AGPL-3.0
 +
 +import { Dispatch } from "redux";
 +import { RootState } from "store/store";
 +import { getUserUuid } from "common/getuser";
 +import { loadDetailsPanel } from "store/details-panel/details-panel-action";
 +import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
 +import { favoritePanelActions, loadFavoritePanel } from "store/favorite-panel/favorite-panel-action";
 +import { getProjectPanelCurrentUuid, setIsProjectPanelTrashed } from "store/project-panel/project-panel-action";
 +import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
 +import {
 +    activateSidePanelTreeItem,
 +    initSidePanelTree,
 +    loadSidePanelTreeProjects,
 +    SidePanelTreeCategory,
 +    SIDE_PANEL_TREE, 
 +} from "store/side-panel-tree/side-panel-tree-actions";
 +import { updateResources } from "store/resources/resources-actions";
 +import { projectPanelColumns } from "views/project-panel/project-panel";
 +import { favoritePanelColumns } from "views/favorite-panel/favorite-panel";
 +import { matchRootRoute } from "routes/routes";
 +import {
 +    setGroupDetailsBreadcrumbs,
 +    setGroupsBreadcrumbs,
 +    setProcessBreadcrumbs,
 +    setSharedWithMeBreadcrumbs,
 +    setSidePanelBreadcrumbs,
 +    setTrashBreadcrumbs,
 +    setUsersBreadcrumbs,
 +    setMyAccountBreadcrumbs,
 +    setUserProfileBreadcrumbs,
 +    setInstanceTypesBreadcrumbs,
 +    setVirtualMachinesBreadcrumbs,
 +    setVirtualMachinesAdminBreadcrumbs,
 +    setRepositoriesBreadcrumbs,
 +} from "store/breadcrumbs/breadcrumbs-actions";
 +import { navigateTo, navigateToRootProject } from "store/navigation/navigation-action";
 +import { MoveToFormDialogData } from "store/move-to-dialog/move-to-dialog";
 +import { ServiceRepository } from "services/services";
 +import { getResource } from "store/resources/resources";
 +import * as projectCreateActions from "store/projects/project-create-actions";
 +import * as projectMoveActions from "store/projects/project-move-actions";
 +import * as projectUpdateActions from "store/projects/project-update-actions";
 +import * as collectionCreateActions from "store/collections/collection-create-actions";
 +import * as collectionCopyActions from "store/collections/collection-copy-actions";
 +import * as collectionMoveActions from "store/collections/collection-move-actions";
 +import * as processesActions from "store/processes/processes-actions";
 +import * as processMoveActions from "store/processes/process-move-actions";
 +import * as processUpdateActions from "store/processes/process-update-actions";
 +import * as processCopyActions from "store/processes/process-copy-actions";
 +import { trashPanelColumns } from "views/trash-panel/trash-panel";
 +import { loadTrashPanel, trashPanelActions } from "store/trash-panel/trash-panel-action";
 +import { loadProcessPanel } from "store/process-panel/process-panel-actions";
 +import { loadSharedWithMePanel, sharedWithMePanelActions } from "store/shared-with-me-panel/shared-with-me-panel-actions";
 +import { sharedWithMePanelColumns } from "views/shared-with-me-panel/shared-with-me-panel";
 +import { CopyFormDialogData } from "store/copy-dialog/copy-dialog";
 +import { workflowPanelActions } from "store/workflow-panel/workflow-panel-actions";
 +import { loadSshKeysPanel } from "store/auth/auth-action-ssh";
 +import { loadLinkAccountPanel, linkAccountPanelActions } from "store/link-account-panel/link-account-panel-actions";
 +import { loadSiteManagerPanel } from "store/auth/auth-action-session";
 +import { workflowPanelColumns } from "views/workflow-panel/workflow-panel-view";
 +import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
 +import { getProgressIndicator } from "store/progress-indicator/progress-indicator-reducer";
 +import { extractUuidKind, Resource, ResourceKind } from "models/resource";
 +import { FilterBuilder } from "services/api/filter-builder";
 +import { GroupContentsResource } from "services/groups-service/groups-service";
 +import { MatchCases, ofType, unionize, UnionOf } from "common/unionize";
 +import { loadRunProcessPanel } from "store/run-process-panel/run-process-panel-actions";
 +import { collectionPanelActions, loadCollectionPanel } from "store/collection-panel/collection-panel-action";
 +import { CollectionResource } from "models/collection";
 +import { WorkflowResource } from "models/workflow";
 +import { loadSearchResultsPanel, searchResultsPanelActions } from "store/search-results-panel/search-results-panel-actions";
 +import { searchResultsPanelColumns } from "views/search-results-panel/search-results-panel-view";
 +import { loadVirtualMachinesPanel } from "store/virtual-machines/virtual-machines-actions";
 +import { loadRepositoriesPanel } from "store/repositories/repositories-actions";
 +import { loadKeepServicesPanel } from "store/keep-services/keep-services-actions";
 +import { loadUsersPanel, userBindedActions } from "store/users/users-actions";
 +import * as userProfilePanelActions from "store/user-profile/user-profile-actions";
 +import { linkPanelActions, loadLinkPanel } from "store/link-panel/link-panel-actions";
 +import { linkPanelColumns } from "views/link-panel/link-panel-root";
 +import { userPanelColumns } from "views/user-panel/user-panel";
 +import { loadApiClientAuthorizationsPanel, apiClientAuthorizationsActions } from "store/api-client-authorizations/api-client-authorizations-actions";
 +import { apiClientAuthorizationPanelColumns } from "views/api-client-authorization-panel/api-client-authorization-panel-root";
 +import * as groupPanelActions from "store/groups-panel/groups-panel-actions";
 +import { groupsPanelColumns } from "views/groups-panel/groups-panel";
 +import * as groupDetailsPanelActions from "store/group-details-panel/group-details-panel-actions";
 +import { groupDetailsMembersPanelColumns, groupDetailsPermissionsPanelColumns } from "views/group-details-panel/group-details-panel";
 +import { DataTableFetchMode } from "components/data-table/data-table";
 +import { loadPublicFavoritePanel, publicFavoritePanelActions } from "store/public-favorites-panel/public-favorites-action";
 +import { publicFavoritePanelColumns } from "views/public-favorites-panel/public-favorites-panel";
 +import {
 +    loadCollectionsContentAddressPanel,
 +    collectionsContentAddressActions,
 +} from "store/collections-content-address-panel/collections-content-address-panel-actions";
 +import { collectionContentAddressPanelColumns } from "views/collection-content-address-panel/collection-content-address-panel";
 +import { subprocessPanelActions } from "store/subprocess-panel/subprocess-panel-actions";
 +import { subprocessPanelColumns } from "views/subprocess-panel/subprocess-panel-root";
 +import { loadAllProcessesPanel, allProcessesPanelActions } from "../all-processes-panel/all-processes-panel-action";
 +import { allProcessesPanelColumns } from "views/all-processes-panel/all-processes-panel";
 +import { userProfileGroupsColumns } from "views/user-profile-panel/user-profile-panel-root";
 +import { selectedToArray, selectedToKindSet } from "components/multiselect-toolbar/MultiselectToolbar";
 +import { multiselectActions } from "store/multiselect/multiselect-actions";
 +import { treePickerActions } from "store/tree-picker/tree-picker-actions";
 +
 +export const WORKBENCH_LOADING_SCREEN = "workbenchLoadingScreen";
 +
 +export const isWorkbenchLoading = (state: RootState) => {
 +    const progress = getProgressIndicator(WORKBENCH_LOADING_SCREEN)(state.progressIndicator);
 +    return progress ? progress.working : false;
 +};
 +
 +export const handleFirstTimeLoad = (action: any) => async (dispatch: Dispatch<any>, getState: () => RootState) => {
 +    try {
 +        await dispatch(action);
 +    } catch (e) {
 +        snackbarActions.OPEN_SNACKBAR({
 +            message: "Error " + e,
 +            hideDuration: 8000,
 +            kind: SnackbarKind.WARNING,
 +        })
 +    } finally {
 +        if (isWorkbenchLoading(getState())) {
 +            dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
 +        }
 +    }
 +};
 +
 +export const loadWorkbench = () => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +    dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
 +    const { auth, router } = getState();
 +    const { user } = auth;
 +    if (user) {
 +        dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
 +        dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
 +        dispatch(
 +            allProcessesPanelActions.SET_COLUMNS({
 +                columns: allProcessesPanelColumns,
 +            })
 +        );
 +        dispatch(
 +            publicFavoritePanelActions.SET_COLUMNS({
 +                columns: publicFavoritePanelColumns,
 +            })
 +        );
 +        dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
 +        dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: sharedWithMePanelColumns }));
 +        dispatch(workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns }));
 +        dispatch(
 +            searchResultsPanelActions.SET_FETCH_MODE({
 +                fetchMode: DataTableFetchMode.INFINITE,
 +            })
 +        );
 +        dispatch(
 +            searchResultsPanelActions.SET_COLUMNS({
 +                columns: searchResultsPanelColumns,
 +            })
 +        );
 +        dispatch(userBindedActions.SET_COLUMNS({ columns: userPanelColumns }));
 +        dispatch(
 +            groupPanelActions.GroupsPanelActions.SET_COLUMNS({
 +                columns: groupsPanelColumns,
 +            })
 +        );
 +        dispatch(
 +            groupDetailsPanelActions.GroupMembersPanelActions.SET_COLUMNS({
 +                columns: groupDetailsMembersPanelColumns,
 +            })
 +        );
 +        dispatch(
 +            groupDetailsPanelActions.GroupPermissionsPanelActions.SET_COLUMNS({
 +                columns: groupDetailsPermissionsPanelColumns,
 +            })
 +        );
 +        dispatch(
 +            userProfilePanelActions.UserProfileGroupsActions.SET_COLUMNS({
 +                columns: userProfileGroupsColumns,
 +            })
 +        );
 +        dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
 +        dispatch(
 +            apiClientAuthorizationsActions.SET_COLUMNS({
 +                columns: apiClientAuthorizationPanelColumns,
 +            })
 +        );
 +        dispatch(
 +            collectionsContentAddressActions.SET_COLUMNS({
 +                columns: collectionContentAddressPanelColumns,
 +            })
 +        );
 +        dispatch(subprocessPanelActions.SET_COLUMNS({ columns: subprocessPanelColumns }));
 +
 +        if (services.linkAccountService.getAccountToLink()) {
 +            dispatch(linkAccountPanelActions.HAS_SESSION_DATA());
 +        }
 +
 +        dispatch<any>(initSidePanelTree());
 +        if (router.location) {
 +            const match = matchRootRoute(router.location.pathname);
 +            if (match) {
 +                dispatch<any>(navigateToRootProject);
 +            }
 +        }
 +    } else {
 +        dispatch(userIsNotAuthenticated);
 +    }
 +};
 +
 +export const loadFavorites = () =>
 +    handleFirstTimeLoad((dispatch: Dispatch) => {
 +        dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.FAVORITES));
 +        dispatch<any>(loadFavoritePanel());
 +        dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
 +    });
 +
 +export const loadCollectionContentAddress = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadCollectionsContentAddressPanel());
 +});
 +
 +export const loadTrash = () =>
 +    handleFirstTimeLoad((dispatch: Dispatch) => {
 +        dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
 +        dispatch<any>(loadTrashPanel());
 +        dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.TRASH));
 +    });
 +
 +export const loadAllProcesses = () =>
 +    handleFirstTimeLoad((dispatch: Dispatch) => {
 +        dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.ALL_PROCESSES));
 +        dispatch<any>(loadAllProcessesPanel());
 +        dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.ALL_PROCESSES));
 +    });
 +
 +export const loadProject = (uuid: string) =>
 +    handleFirstTimeLoad(async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
 +        const userUuid = getUserUuid(getState());
 +        dispatch(setIsProjectPanelTrashed(false));
 +        if (!userUuid) {
 +            return;
 +        }
 +        try {
 +            dispatch(progressIndicatorActions.START_WORKING(uuid));
 +            if (extractUuidKind(uuid) === ResourceKind.USER && userUuid !== uuid) {
 +                // Load another users home projects
 +                dispatch(finishLoadingProject(uuid));
 +            } else if (userUuid !== uuid) {
 +                await dispatch(finishLoadingProject(uuid));
 +                const match = await loadGroupContentsResource({
 +                    uuid,
 +                    userUuid,
 +                    services,
 +                });
 +                match({
 +                    OWNED: async () => {
 +                        await dispatch(activateSidePanelTreeItem(uuid));
 +                        dispatch<any>(setSidePanelBreadcrumbs(uuid));
 +                    },
 +                    SHARED: async () => {
 +                        await dispatch(activateSidePanelTreeItem(uuid));
 +                        dispatch<any>(setSharedWithMeBreadcrumbs(uuid));
 +                    },
 +                    TRASHED: async () => {
 +                        await dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
 +                        dispatch<any>(setTrashBreadcrumbs(uuid));
 +                        dispatch(setIsProjectPanelTrashed(true));
 +                    },
 +                });
 +            } else {
 +                await dispatch(finishLoadingProject(userUuid));
 +                await dispatch(activateSidePanelTreeItem(userUuid));
 +                dispatch<any>(setSidePanelBreadcrumbs(userUuid));
 +            }
 +        } finally {
 +            dispatch(progressIndicatorActions.STOP_WORKING(uuid));
 +        }
 +    });
 +
 +export const createProject = (data: projectCreateActions.ProjectCreateFormDialogData) => async (dispatch: Dispatch) => {
 +    const newProject = await dispatch<any>(projectCreateActions.createProject(data));
 +    if (newProject) {
 +        dispatch(
 +            snackbarActions.OPEN_SNACKBAR({
 +                message: "Project has been successfully created.",
 +                hideDuration: 2000,
 +                kind: SnackbarKind.SUCCESS,
 +            })
 +        );
 +        await dispatch<any>(loadSidePanelTreeProjects(newProject.ownerUuid));
 +        dispatch<any>(navigateTo(newProject.uuid));
 +    }
 +};
 +
 +export const moveProject =
 +    (data: MoveToFormDialogData, isSecondaryMove = false) =>
 +        async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +            const checkedList = getState().multiselect.checkedList;
 +            const uuidsToMove: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
 +
 +            //if no items in checkedlist default to normal context menu behavior
 +            if (!isSecondaryMove && !uuidsToMove.length) uuidsToMove.push(data.uuid);
 +
 +            const sourceUuid = getResource(data.uuid)(getState().resources)?.ownerUuid;
 +            const destinationUuid = data.ownerUuid;
 +
 +            const projectsToMove: MoveableResource[] = uuidsToMove
 +                .map(uuid => getResource(uuid)(getState().resources) as MoveableResource)
 +                .filter(resource => resource.kind === ResourceKind.PROJECT);
 +
 +            for (const project of projectsToMove) {
 +                await moveSingleProject(project);
 +            }
 +
 +            //omly propagate if this call is the original
 +            if (!isSecondaryMove) {
 +                const kindsToMove: Set<string> = selectedToKindSet(checkedList);
 +                kindsToMove.delete(ResourceKind.PROJECT);
 +
 +                kindsToMove.forEach(kind => {
 +                    secondaryMove[kind](data, true)(dispatch, getState, services);
 +                });
 +            }
 +
 +            async function moveSingleProject(project: MoveableResource) {
 +                try {
 +                    const oldProject: MoveToFormDialogData = { name: project.name, uuid: project.uuid, ownerUuid: data.ownerUuid };
 +                    const oldOwnerUuid = oldProject ? oldProject.ownerUuid : "";
 +                    const movedProject = await dispatch<any>(projectMoveActions.moveProject(oldProject));
 +                    if (movedProject) {
 +                        dispatch(
 +                            snackbarActions.OPEN_SNACKBAR({
 +                                message: "Project has been moved",
 +                                hideDuration: 2000,
 +                                kind: SnackbarKind.SUCCESS,
 +                            })
 +                        );
 +                        await dispatch<any>(reloadProjectMatchingUuid([oldOwnerUuid, movedProject.ownerUuid, movedProject.uuid]));
 +                    }
 +                } catch (e) {
 +                    dispatch(
 +                        snackbarActions.OPEN_SNACKBAR({
++                            message: !!(project as any).frozenByUuid ? 'Could not move frozen project.' : e.message,
 +                            hideDuration: 2000,
 +                            kind: SnackbarKind.ERROR,
 +                        })
 +                    );
 +                }
 +            }
 +            if (sourceUuid) await dispatch<any>(loadSidePanelTreeProjects(sourceUuid));
 +            await dispatch<any>(loadSidePanelTreeProjects(destinationUuid));
 +        };
 +
 +export const updateProject = (data: projectUpdateActions.ProjectUpdateFormDialogData) => async (dispatch: Dispatch) => {
 +    const updatedProject = await dispatch<any>(projectUpdateActions.updateProject(data));
 +    if (updatedProject) {
 +        dispatch(
 +            snackbarActions.OPEN_SNACKBAR({
 +                message: "Project has been successfully updated.",
 +                hideDuration: 2000,
 +                kind: SnackbarKind.SUCCESS,
 +            })
 +        );
 +        await dispatch<any>(loadSidePanelTreeProjects(updatedProject.ownerUuid));
 +        dispatch<any>(reloadProjectMatchingUuid([updatedProject.ownerUuid, updatedProject.uuid]));
 +    }
 +};
 +
 +export const updateGroup = (data: projectUpdateActions.ProjectUpdateFormDialogData) => async (dispatch: Dispatch) => {
 +    const updatedGroup = await dispatch<any>(groupPanelActions.updateGroup(data));
 +    if (updatedGroup) {
 +        dispatch(
 +            snackbarActions.OPEN_SNACKBAR({
 +                message: "Group has been successfully updated.",
 +                hideDuration: 2000,
 +                kind: SnackbarKind.SUCCESS,
 +            })
 +        );
 +        await dispatch<any>(loadSidePanelTreeProjects(updatedGroup.ownerUuid));
 +        dispatch<any>(reloadProjectMatchingUuid([updatedGroup.ownerUuid, updatedGroup.uuid]));
 +    }
 +};
 +
 +export const loadCollection = (uuid: string) =>
 +    handleFirstTimeLoad(async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
 +        const userUuid = getUserUuid(getState());
 +        try {
 +            dispatch(progressIndicatorActions.START_WORKING(uuid));
 +            if (userUuid) {
 +                const match = await loadGroupContentsResource({
 +                    uuid,
 +                    userUuid,
 +                    services,
 +                });
 +                let collection: CollectionResource | undefined;
 +                let breadcrumbfunc:
 +                    | ((uuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => Promise<void>)
 +                    | undefined;
 +                let sidepanel: string | undefined;
 +                match({
 +                    OWNED: thecollection => {
 +                        collection = thecollection as CollectionResource;
 +                        sidepanel = collection.ownerUuid;
 +                        breadcrumbfunc = setSidePanelBreadcrumbs;
 +                    },
 +                    SHARED: thecollection => {
 +                        collection = thecollection as CollectionResource;
 +                        sidepanel = collection.ownerUuid;
 +                        breadcrumbfunc = setSharedWithMeBreadcrumbs;
 +                    },
 +                    TRASHED: thecollection => {
 +                        collection = thecollection as CollectionResource;
 +                        sidepanel = SidePanelTreeCategory.TRASH;
 +                        breadcrumbfunc = () => setTrashBreadcrumbs("");
 +                    },
 +                });
 +                if (collection && breadcrumbfunc && sidepanel) {
 +                    dispatch(updateResources([collection]));
 +                    await dispatch<any>(finishLoadingProject(collection.ownerUuid));
 +                    dispatch(collectionPanelActions.SET_COLLECTION(collection));
 +                    await dispatch(activateSidePanelTreeItem(sidepanel));
 +                    dispatch(breadcrumbfunc(collection.ownerUuid));
 +                    dispatch(loadCollectionPanel(collection.uuid));
 +                }
 +            }
 +        } finally {
 +            dispatch(progressIndicatorActions.STOP_WORKING(uuid));
 +        }
 +    });
 +
 +export const createCollection = (data: collectionCreateActions.CollectionCreateFormDialogData) => async (dispatch: Dispatch) => {
 +    const collection = await dispatch<any>(collectionCreateActions.createCollection(data));
 +    if (collection) {
 +        dispatch(
 +            snackbarActions.OPEN_SNACKBAR({
 +                message: "Collection has been successfully created.",
 +                hideDuration: 2000,
 +                kind: SnackbarKind.SUCCESS,
 +            })
 +        );
 +        dispatch<any>(updateResources([collection]));
 +        dispatch<any>(navigateTo(collection.uuid));
 +    }
 +};
 +
 +export const copyCollection = (data: CopyFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +    const checkedList = getState().multiselect.checkedList;
 +    const uuidsToCopy: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
 +
 +    //if no items in checkedlist && no items passed in, default to normal context menu behavior
 +    if (!uuidsToCopy.length) uuidsToCopy.push(data.uuid);
 +
 +    const collectionsToCopy: CollectionCopyResource[] = uuidsToCopy
 +        .map(uuid => getResource(uuid)(getState().resources) as CollectionCopyResource)
 +        .filter(resource => resource.kind === ResourceKind.COLLECTION);
 +
 +    for (const collection of collectionsToCopy) {
 +        await copySingleCollection({ ...collection, ownerUuid: data.ownerUuid } as CollectionCopyResource);
 +    }
 +
 +    async function copySingleCollection(copyToProject: CollectionCopyResource) {
 +        const newName = data.fromContextMenu || collectionsToCopy.length === 1 ? data.name : `Copy of: ${copyToProject.name}`;
 +        try {
 +            const collection = await dispatch<any>(
 +                collectionCopyActions.copyCollection({
 +                    ...copyToProject,
 +                    name: newName,
 +                    fromContextMenu: collectionsToCopy.length === 1 ? true : data.fromContextMenu,
 +                })
 +            );
 +            if (copyToProject && collection) {
 +                await dispatch<any>(reloadProjectMatchingUuid([copyToProject.uuid]));
 +                dispatch(
 +                    snackbarActions.OPEN_SNACKBAR({
 +                        message: "Collection has been copied.",
 +                        hideDuration: 3000,
 +                        kind: SnackbarKind.SUCCESS,
 +                        link: collection.ownerUuid,
 +                    })
 +                );
 +                dispatch<any>(multiselectActions.deselectOne(copyToProject.uuid));
 +            }
 +        } catch (e) {
 +            dispatch(
 +                snackbarActions.OPEN_SNACKBAR({
 +                    message: e.message,
 +                    hideDuration: 2000,
 +                    kind: SnackbarKind.ERROR,
 +                })
 +            );
 +        }
 +    }
 +    dispatch(projectPanelActions.REQUEST_ITEMS());
 +};
 +
 +export const moveCollection =
 +    (data: MoveToFormDialogData, isSecondaryMove = false) =>
 +        async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +            const checkedList = getState().multiselect.checkedList;
 +            const uuidsToMove: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
 +
 +            //if no items in checkedlist && no items passed in, default to normal context menu behavior
 +            if (!isSecondaryMove && !uuidsToMove.length) uuidsToMove.push(data.uuid);
 +
 +            const collectionsToMove: MoveableResource[] = uuidsToMove
 +                .map(uuid => getResource(uuid)(getState().resources) as MoveableResource)
 +                .filter(resource => resource.kind === ResourceKind.COLLECTION);
 +
 +            for (const collection of collectionsToMove) {
 +                await moveSingleCollection(collection);
 +            }
 +
 +            //omly propagate if this call is the original
 +            if (!isSecondaryMove) {
 +                const kindsToMove: Set<string> = selectedToKindSet(checkedList);
 +                kindsToMove.delete(ResourceKind.COLLECTION);
 +
 +                kindsToMove.forEach(kind => {
 +                    secondaryMove[kind](data, true)(dispatch, getState, services);
 +                });
 +            }
 +
 +            async function moveSingleCollection(collection: MoveableResource) {
 +                try {
 +                    const oldCollection: MoveToFormDialogData = { name: collection.name, uuid: collection.uuid, ownerUuid: data.ownerUuid };
 +                    const movedCollection = await dispatch<any>(collectionMoveActions.moveCollection(oldCollection));
 +                    dispatch<any>(updateResources([movedCollection]));
 +                    dispatch<any>(reloadProjectMatchingUuid([movedCollection.ownerUuid]));
 +                    dispatch(
 +                        snackbarActions.OPEN_SNACKBAR({
 +                            message: "Collection has been moved.",
 +                            hideDuration: 2000,
 +                            kind: SnackbarKind.SUCCESS,
 +                        })
 +                    );
 +                } catch (e) {
 +                    dispatch(
 +                        snackbarActions.OPEN_SNACKBAR({
 +                            message: e.message,
 +                            hideDuration: 2000,
 +                            kind: SnackbarKind.ERROR,
 +                        })
 +                    );
 +                }
 +            }
 +        };
 +
 +export const loadProcess = (uuid: string) =>
 +    handleFirstTimeLoad(async (dispatch: Dispatch, getState: () => RootState) => {
 +        try {
 +            dispatch(progressIndicatorActions.START_WORKING(uuid));
 +            dispatch<any>(loadProcessPanel(uuid));
 +            const process = await dispatch<any>(processesActions.loadProcess(uuid));
 +            if (process) {
 +                await dispatch<any>(finishLoadingProject(process.containerRequest.ownerUuid));
 +                await dispatch<any>(activateSidePanelTreeItem(process.containerRequest.ownerUuid));
 +                dispatch<any>(setProcessBreadcrumbs(uuid));
 +                dispatch<any>(loadDetailsPanel(uuid));
 +            }
 +        } finally {
 +            dispatch(progressIndicatorActions.STOP_WORKING(uuid));
 +        }
 +    });
 +
 +export const loadRegisteredWorkflow = (uuid: string) =>
 +    handleFirstTimeLoad(async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +        const userUuid = getUserUuid(getState());
 +        if (userUuid) {
 +            const match = await loadGroupContentsResource({
 +                uuid,
 +                userUuid,
 +                services,
 +            });
 +            let workflow: WorkflowResource | undefined;
 +            let breadcrumbfunc:
 +                | ((uuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => Promise<void>)
 +                | undefined;
 +            match({
 +                OWNED: async theworkflow => {
 +                    workflow = theworkflow as WorkflowResource;
 +                    breadcrumbfunc = setSidePanelBreadcrumbs;
 +                },
 +                SHARED: async theworkflow => {
 +                    workflow = theworkflow as WorkflowResource;
 +                    breadcrumbfunc = setSharedWithMeBreadcrumbs;
 +                },
 +                TRASHED: () => { },
 +            });
 +            if (workflow && breadcrumbfunc) {
 +                dispatch(updateResources([workflow]));
 +                await dispatch<any>(finishLoadingProject(workflow.ownerUuid));
 +                await dispatch<any>(activateSidePanelTreeItem(workflow.ownerUuid));
 +                dispatch<any>(breadcrumbfunc(workflow.ownerUuid));
 +            }
 +        }
 +    });
 +
 +export const updateProcess = (data: processUpdateActions.ProcessUpdateFormDialogData) => async (dispatch: Dispatch) => {
 +    try {
 +        const process = await dispatch<any>(processUpdateActions.updateProcess(data));
 +        if (process) {
 +            dispatch(
 +                snackbarActions.OPEN_SNACKBAR({
 +                    message: "Process has been successfully updated.",
 +                    hideDuration: 2000,
 +                    kind: SnackbarKind.SUCCESS,
 +                })
 +            );
 +            dispatch<any>(updateResources([process]));
 +            dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
 +        }
 +    } catch (e) {
 +        dispatch(
 +            snackbarActions.OPEN_SNACKBAR({
 +                message: e.message,
 +                hideDuration: 2000,
 +                kind: SnackbarKind.ERROR,
 +            })
 +        );
 +    }
 +};
 +
 +export const moveProcess =
 +    (data: MoveToFormDialogData, isSecondaryMove = false) =>
 +        async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +            const checkedList = getState().multiselect.checkedList;
 +            const uuidsToMove: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
 +
 +            //if no items in checkedlist && no items passed in, default to normal context menu behavior
 +            if (!isSecondaryMove && !uuidsToMove.length) uuidsToMove.push(data.uuid);
 +
 +            const processesToMove: MoveableResource[] = uuidsToMove
 +                .map(uuid => getResource(uuid)(getState().resources) as MoveableResource)
 +                .filter(resource => resource.kind === ResourceKind.PROCESS);
 +
 +            for (const process of processesToMove) {
 +                await moveSingleProcess(process);
 +            }
 +
 +            //omly propagate if this call is the original
 +            if (!isSecondaryMove) {
 +                const kindsToMove: Set<string> = selectedToKindSet(checkedList);
 +                kindsToMove.delete(ResourceKind.PROCESS);
 +
 +                kindsToMove.forEach(kind => {
 +                    secondaryMove[kind](data, true)(dispatch, getState, services);
 +                });
 +            }
 +
 +            async function moveSingleProcess(process: MoveableResource) {
 +                try {
 +                    const oldProcess: MoveToFormDialogData = { name: process.name, uuid: process.uuid, ownerUuid: data.ownerUuid };
 +                    const movedProcess = await dispatch<any>(processMoveActions.moveProcess(oldProcess));
 +                    dispatch<any>(updateResources([movedProcess]));
 +                    dispatch<any>(reloadProjectMatchingUuid([movedProcess.ownerUuid]));
 +                    dispatch(
 +                        snackbarActions.OPEN_SNACKBAR({
 +                            message: "Process has been moved.",
 +                            hideDuration: 2000,
 +                            kind: SnackbarKind.SUCCESS,
 +                        })
 +                    );
 +                } catch (e) {
 +                    dispatch(
 +                        snackbarActions.OPEN_SNACKBAR({
 +                            message: e.message,
 +                            hideDuration: 2000,
 +                            kind: SnackbarKind.ERROR,
 +                        })
 +                    );
 +                }
 +            }
 +        };
 +
 +export const copyProcess = (data: CopyFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +    try {
 +        const process = await dispatch<any>(processCopyActions.copyProcess(data));
 +        dispatch<any>(updateResources([process]));
 +        dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
 +        dispatch(
 +            snackbarActions.OPEN_SNACKBAR({
 +                message: "Process has been copied.",
 +                hideDuration: 2000,
 +                kind: SnackbarKind.SUCCESS,
 +            })
 +        );
 +        dispatch<any>(navigateTo(process.uuid));
 +    } catch (e) {
 +        dispatch(
 +            snackbarActions.OPEN_SNACKBAR({
 +                message: e.message,
 +                hideDuration: 2000,
 +                kind: SnackbarKind.ERROR,
 +            })
 +        );
 +    }
 +};
 +
 +export const resourceIsNotLoaded = (uuid: string) =>
 +    snackbarActions.OPEN_SNACKBAR({
 +        message: `Resource identified by ${uuid} is not loaded.`,
 +        kind: SnackbarKind.ERROR,
 +    });
 +
 +export const userIsNotAuthenticated = snackbarActions.OPEN_SNACKBAR({
 +    message: "User is not authenticated",
 +    kind: SnackbarKind.ERROR,
 +});
 +
 +export const couldNotLoadUser = snackbarActions.OPEN_SNACKBAR({
 +    message: "Could not load user",
 +    kind: SnackbarKind.ERROR,
 +});
 +
 +export const reloadProjectMatchingUuid =
 +    (matchingUuids: string[]) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +        const currentProjectPanelUuid = getProjectPanelCurrentUuid(getState());
 +        if (currentProjectPanelUuid && matchingUuids.some(uuid => uuid === currentProjectPanelUuid)) {
 +            dispatch<any>(loadProject(currentProjectPanelUuid));
 +        }
 +    };
 +
 +export const loadSharedWithMe = handleFirstTimeLoad(async (dispatch: Dispatch) => {
 +    dispatch<any>(loadSharedWithMePanel());
 +    await dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.SHARED_WITH_ME));
 +    await dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.SHARED_WITH_ME));
 +});
 +
 +export const loadRunProcess = handleFirstTimeLoad(async (dispatch: Dispatch) => {
 +    await dispatch<any>(loadRunProcessPanel());
 +});
 +
 +export const loadPublicFavorites = () =>
 +    handleFirstTimeLoad((dispatch: Dispatch) => {
 +        dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.PUBLIC_FAVORITES));
 +        dispatch<any>(loadPublicFavoritePanel());
 +        dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.PUBLIC_FAVORITES));
 +    });
 +
 +export const loadSearchResults = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadSearchResultsPanel());
 +});
 +
 +export const loadLinks = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadLinkPanel());
 +});
 +
 +export const loadVirtualMachines = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadVirtualMachinesPanel());
 +    dispatch(setVirtualMachinesBreadcrumbs());
 +    dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.SHELL_ACCESS));
 +});
 +
 +export const loadVirtualMachinesAdmin = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadVirtualMachinesPanel());
 +    dispatch(setVirtualMachinesAdminBreadcrumbs());
 +    dispatch(treePickerActions.DEACTIVATE_TREE_PICKER_NODE({pickerId: SIDE_PANEL_TREE} ))
 +});
 +
 +export const loadRepositories = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadRepositoriesPanel());
 +    dispatch(setRepositoriesBreadcrumbs());
 +});
 +
 +export const loadSshKeys = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadSshKeysPanel());
 +});
 +
 +export const loadInstanceTypes = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.INSTANCE_TYPES));
 +    dispatch(setInstanceTypesBreadcrumbs());
 +});
 +
 +export const loadSiteManager = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadSiteManagerPanel());
 +});
 +
 +export const loadUserProfile = (userUuid?: string) =>
 +    handleFirstTimeLoad((dispatch: Dispatch<any>) => {
 +        if (userUuid) {
 +            dispatch(setUserProfileBreadcrumbs(userUuid));
 +            dispatch(userProfilePanelActions.loadUserProfilePanel(userUuid));
 +        } else {
 +            dispatch(setMyAccountBreadcrumbs());
 +            dispatch(userProfilePanelActions.loadUserProfilePanel());
 +        }
 +    });
 +
 +export const loadLinkAccount = handleFirstTimeLoad((dispatch: Dispatch<any>) => {
 +    dispatch(loadLinkAccountPanel());
 +});
 +
 +export const loadKeepServices = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadKeepServicesPanel());
 +});
 +
 +export const loadUsers = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadUsersPanel());
 +    dispatch(setUsersBreadcrumbs());
 +});
 +
 +export const loadApiClientAuthorizations = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadApiClientAuthorizationsPanel());
 +});
 +
 +export const loadGroupsPanel = handleFirstTimeLoad((dispatch: Dispatch<any>) => {
 +    dispatch(setGroupsBreadcrumbs());
 +    dispatch(groupPanelActions.loadGroupsPanel());
 +});
 +
 +export const loadGroupDetailsPanel = (groupUuid: string) =>
 +    handleFirstTimeLoad((dispatch: Dispatch<any>) => {
 +        dispatch(setGroupDetailsBreadcrumbs(groupUuid));
 +        dispatch(groupDetailsPanelActions.loadGroupDetailsPanel(groupUuid));
 +    });
 +
 +const finishLoadingProject = (project: GroupContentsResource | string) => async (dispatch: Dispatch<any>) => {
 +    const uuid = typeof project === "string" ? project : project.uuid;
 +    dispatch(loadDetailsPanel(uuid));
 +    if (typeof project !== "string") {
 +        dispatch(updateResources([project]));
 +    }
 +};
 +
 +const loadGroupContentsResource = async (params: { uuid: string; userUuid: string; services: ServiceRepository }) => {
 +    const filters = new FilterBuilder().addEqual("uuid", params.uuid).getFilters();
 +    const { items } = await params.services.groupsService.contents(params.userUuid, {
 +        filters,
 +        recursive: true,
 +        includeTrash: true,
 +    });
 +    const resource = items.shift();
 +    let handler: GroupContentsHandler;
 +    if (resource) {
 +        handler =
 +            (resource.kind === ResourceKind.COLLECTION || resource.kind === ResourceKind.PROJECT) && resource.isTrashed
 +                ? groupContentsHandlers.TRASHED(resource)
 +                : groupContentsHandlers.OWNED(resource);
 +    } else {
 +        const kind = extractUuidKind(params.uuid);
 +        let resource: GroupContentsResource;
 +        if (kind === ResourceKind.COLLECTION) {
 +            resource = await params.services.collectionService.get(params.uuid);
 +        } else if (kind === ResourceKind.PROJECT) {
 +            resource = await params.services.projectService.get(params.uuid);
 +        } else if (kind === ResourceKind.WORKFLOW) {
 +            resource = await params.services.workflowService.get(params.uuid);
 +        } else if (kind === ResourceKind.CONTAINER_REQUEST) {
 +            resource = await params.services.containerRequestService.get(params.uuid);
 +        } else {
 +            throw new Error("loadGroupContentsResource unsupported kind " + kind);
 +        }
 +        handler = groupContentsHandlers.SHARED(resource);
 +    }
 +    return (cases: MatchCases<typeof groupContentsHandlersRecord, GroupContentsHandler, void>) => groupContentsHandlers.match(handler, cases);
 +};
 +
 +const groupContentsHandlersRecord = {
 +    TRASHED: ofType<GroupContentsResource>(),
 +    SHARED: ofType<GroupContentsResource>(),
 +    OWNED: ofType<GroupContentsResource>(),
 +};
 +
 +const groupContentsHandlers = unionize(groupContentsHandlersRecord);
 +
 +type GroupContentsHandler = UnionOf<typeof groupContentsHandlers>;
 +
 +type CollectionCopyResource = Resource & { name: string; fromContextMenu: boolean };
 +
 +type MoveableResource = Resource & { name: string };
 +
 +type MoveFunc = (
 +    data: MoveToFormDialogData,
 +    isSecondaryMove?: boolean
 +) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => Promise<void>;
 +
 +const secondaryMove: Record<string, MoveFunc> = {
 +    [ResourceKind.PROJECT]: moveProject,
 +    [ResourceKind.PROCESS]: moveProcess,
 +    [ResourceKind.COLLECTION]: moveCollection,
 +};
index 64b90ff45c5d84a57b7eb832831b5bf0667dee45,2aa7faa1242369be4ea985bad80805b94529b72f..2aa7faa1242369be4ea985bad80805b94529b72f
@@@ -60,8 -60,8 +60,8 @@@ export const readOnlyProcessResourceAct
              icon: OutputIcon,
              name: "Outputs",
              execute: (dispatch, resources) => {
-                 if (resources[0].outputUuid) {
-                     dispatch<any>(navigateToOutput(resources[0].outputUuid));
+                 if (resources[0]) {
+                     dispatch<any>(navigateToOutput(resources[0]));
                  }
              },
          },
index 2a5cccc0a549231d2860ec93bb5beec9d5435c1e,aeb69de7624bf3e27a82f7a911f7de1d57e03d4c..aeb69de7624bf3e27a82f7a911f7de1d57e03d4c
@@@ -16,7 -16,6 +16,6 @@@ type DataProps = Pick<ContextMenuProps
  
  const mapStateToProps = (state: RootState): DataProps => {
      const { open, position, resource } = state.contextMenu;
      const filteredItems = getMenuActionSet(resource).map(group =>
          group.filter(item => {
              if (resource && item.filters) {
index d5a9977a71b4caad93d464d43d580ca457399e6c,2e316f68598b87b923122c905e633a1ad09ebba3..2e316f68598b87b923122c905e633a1ad09ebba3
@@@ -22,13 -22,14 +22,14 @@@ interface Props 
      extractKey?: (item: any) => React.Key;
  }
  
- const mapStateToProps = (state: RootState, { id }: Props) => {
-     const progress = state.progressIndicator.find(p => p.id === id);
-     const dataExplorerState = getDataExplorer(state.dataExplorer, id);
-     const currentRoute = state.router.location ? state.router.location.pathname : "";
+ const mapStateToProps = ({ progressIndicator, dataExplorer, router, multiselect, detailsPanel, properties}: RootState, { id }: Props) => {
+     const progress = progressIndicator.find(p => p.id === id);
+     const dataExplorerState = getDataExplorer(dataExplorer, id);
+     const currentRoute = router.location ? router.location.pathname : "";
      const currentRefresh = localStorage.getItem(LAST_REFRESH_TIMESTAMP) || "";
-     const currentItemUuid = currentRoute === "/workflows" ? state.properties.workflowPanelDetailsUuid : state.detailsPanel.resourceUuid;
-     const isMSToolbarVisible = state.multiselect.isVisible;
+     const isDetailsResourceChecked = multiselect.checkedList[detailsPanel.resourceUuid]
+     const currentItemUuid = currentRoute === "/workflows" ? properties.workflowPanelDetailsUuid : isDetailsResourceChecked ? detailsPanel.resourceUuid : multiselect.selectedUuid;
+     const isMSToolbarVisible = multiselect.isVisible;
      return {
          ...dataExplorerState,
          working: !!progress?.working,
          paperKey: currentRoute,
          currentItemUuid,
          isMSToolbarVisible,
-         checkedList: state.multiselect.checkedList,
+         checkedList: multiselect.checkedList,
      };
  };
  
- const mapDispatchToProps = dispatchFn => {
+ const mapDispatchToProps = () => {
      return (dispatch: Dispatch, { id, onRowClick, onRowDoubleClick, onContextMenu }: Props) => ({
          onSetColumns: (columns: DataColumns<any, any>) => {
              dispatch(dataExplorerActions.SET_COLUMNS({ id, columns }));
index 0ebe96ef3acb56f0ea29b3fdbdcdf3d40276b738,0000000000000000000000000000000000000000..56926b513db459dbe818130828761c514ee6fbe9
mode 100644,000000..100644
--- /dev/null
@@@ -1,1134 -1,0 +1,1137 @@@
-                     onClick={() => dispatch<any>(navFunc(item.uuid))}
 +// Copyright (C) The Arvados Authors. All rights reserved.
 +//
 +// SPDX-License-Identifier: AGPL-3.0
 +
 +import React from "react";
 +import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox, Chip } from "@material-ui/core";
 +import { FavoriteStar, PublicFavoriteStar } from "../favorite-star/favorite-star";
 +import { Resource, ResourceKind, TrashableResource } from "models/resource";
 +import {
 +    FreezeIcon,
 +    ProjectIcon,
 +    FilterGroupIcon,
 +    CollectionIcon,
 +    ProcessIcon,
 +    DefaultIcon,
 +    ShareIcon,
 +    CollectionOldVersionIcon,
 +    WorkflowIcon,
 +    RemoveIcon,
 +    RenameIcon,
 +    ActiveIcon,
 +    SetupIcon,
 +    InactiveIcon,
 +} from "components/icon/icon";
 +import { formatDate, formatFileSize, formatTime } from "common/formatters";
 +import { resourceLabel } from "common/labels";
 +import { connect, DispatchProp } from "react-redux";
 +import { RootState } from "store/store";
 +import { getResource, filterResources } from "store/resources/resources";
 +import { GroupContentsResource } from "services/groups-service/groups-service";
 +import { getProcess, Process, getProcessStatus, getProcessStatusStyles, getProcessRuntime } from "store/processes/process";
 +import { ArvadosTheme } from "common/custom-theme";
 +import { compose, Dispatch } from "redux";
 +import { WorkflowResource } from "models/workflow";
 +import { ResourceStatus as WorkflowStatus } from "views/workflow-panel/workflow-panel-view";
 +import { getUuidPrefix, openRunProcess } from "store/workflow-panel/workflow-panel-actions";
 +import { openSharingDialog } from "store/sharing-dialog/sharing-dialog-actions";
 +import { getUserFullname, getUserDisplayName, User, UserResource } from "models/user";
 +import { toggleIsAdmin } from "store/users/users-actions";
 +import { LinkClass, LinkResource } from "models/link";
 +import { navigateTo, navigateToGroupDetails, navigateToUserProfile } from "store/navigation/navigation-action";
 +import { withResourceData } from "views-components/data-explorer/with-resources";
 +import { CollectionResource } from "models/collection";
 +import { IllegalNamingWarning } from "components/warning/warning";
 +import { loadResource } from "store/resources/resources-actions";
 +import { BuiltinGroups, getBuiltinGroupUuid, GroupClass, GroupResource, isBuiltinGroup } from "models/group";
 +import { openRemoveGroupMemberDialog } from "store/group-details-panel/group-details-panel-actions";
 +import { setMemberIsHidden } from "store/group-details-panel/group-details-panel-actions";
 +import { formatPermissionLevel } from "views-components/sharing-dialog/permission-select";
 +import { PermissionLevel } from "models/permission";
 +import { openPermissionEditContextMenu } from "store/context-menu/context-menu-actions";
 +import { VirtualMachinesResource } from "models/virtual-machines";
 +import { CopyToClipboardSnackbar } from "components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar";
 +import { ProjectResource } from "models/project";
 +import { ProcessResource } from "models/process";
 +
 +const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
 +    const navFunc = "groupClass" in item && item.groupClass === GroupClass.ROLE ? navigateToGroupDetails : navigateTo;
 +    return (
 +        <Grid
 +            container
 +            alignItems="center"
 +            wrap="nowrap"
 +            spacing={16}
 +        >
 +            <Grid item>{renderIcon(item)}</Grid>
 +            <Grid item>
 +                <Typography
 +                    color="primary"
 +                    style={{ width: "auto", cursor: "pointer" }}
++                    onClick={(ev) => {
++                        ev.stopPropagation()
++                        dispatch<any>(navFunc(item.uuid))
++                    }}
 +                >
 +                    {item.kind === ResourceKind.PROJECT || item.kind === ResourceKind.COLLECTION ? <IllegalNamingWarning name={item.name} /> : null}
 +                    {item.name}
 +                </Typography>
 +            </Grid>
 +            <Grid item>
 +                <Typography variant="caption">
 +                    <FavoriteStar resourceUuid={item.uuid} />
 +                    <PublicFavoriteStar resourceUuid={item.uuid} />
 +                    {item.kind === ResourceKind.PROJECT && <FrozenProject item={item} />}
 +                </Typography>
 +            </Grid>
 +        </Grid>
 +    );
 +};
 +
 +const FrozenProject = (props: { item: ProjectResource }) => {
 +    const [fullUsername, setFullusername] = React.useState<any>(null);
 +    const getFullName = React.useCallback(() => {
 +        if (props.item.frozenByUuid) {
 +            setFullusername(<UserNameFromID uuid={props.item.frozenByUuid} />);
 +        }
 +    }, [props.item, setFullusername]);
 +
 +    if (props.item.frozenByUuid) {
 +        return (
 +            <Tooltip
 +                onOpen={getFullName}
 +                enterDelay={500}
 +                title={<span>Project was frozen by {fullUsername}</span>}
 +            >
 +                <FreezeIcon style={{ fontSize: "inherit" }} />
 +            </Tooltip>
 +        );
 +    } else {
 +        return null;
 +    }
 +};
 +
 +export const ResourceName = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
 +    return resource;
 +})((resource: GroupContentsResource & DispatchProp<any>) => renderName(resource.dispatch, resource));
 +
 +const renderIcon = (item: GroupContentsResource) => {
 +    switch (item.kind) {
 +        case ResourceKind.PROJECT:
 +            if (item.groupClass === GroupClass.FILTER) {
 +                return <FilterGroupIcon />;
 +            }
 +            return <ProjectIcon />;
 +        case ResourceKind.COLLECTION:
 +            if (item.uuid === item.currentVersionUuid) {
 +                return <CollectionIcon />;
 +            }
 +            return <CollectionOldVersionIcon />;
 +        case ResourceKind.PROCESS:
 +            return <ProcessIcon />;
 +        case ResourceKind.WORKFLOW:
 +            return <WorkflowIcon />;
 +        default:
 +            return <DefaultIcon />;
 +    }
 +};
 +
 +const renderDate = (date?: string) => {
 +    return (
 +        <Typography
 +            noWrap
 +            style={{ minWidth: "100px" }}
 +        >
 +            {formatDate(date)}
 +        </Typography>
 +    );
 +};
 +
 +const renderWorkflowName = (item: WorkflowResource) => (
 +    <Grid
 +        container
 +        alignItems="center"
 +        wrap="nowrap"
 +        spacing={16}
 +    >
 +        <Grid item>{renderIcon(item)}</Grid>
 +        <Grid item>
 +            <Typography
 +                color="primary"
 +                style={{ width: "100px" }}
 +            >
 +                {item.name}
 +            </Typography>
 +        </Grid>
 +    </Grid>
 +);
 +
 +export const ResourceWorkflowName = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
 +    return resource;
 +})(renderWorkflowName);
 +
 +const getPublicUuid = (uuidPrefix: string) => {
 +    return `${uuidPrefix}-tpzed-anonymouspublic`;
 +};
 +
 +const resourceShare = (dispatch: Dispatch, uuidPrefix: string, ownerUuid?: string, uuid?: string) => {
 +    const isPublic = ownerUuid === getPublicUuid(uuidPrefix);
 +    return (
 +        <div>
 +            {!isPublic && uuid && (
 +                <Tooltip title="Share">
 +                    <IconButton onClick={() => dispatch<any>(openSharingDialog(uuid))}>
 +                        <ShareIcon />
 +                    </IconButton>
 +                </Tooltip>
 +            )}
 +        </div>
 +    );
 +};
 +
 +export const ResourceShare = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
 +    const uuidPrefix = getUuidPrefix(state);
 +    return {
 +        uuid: resource ? resource.uuid : "",
 +        ownerUuid: resource ? resource.ownerUuid : "",
 +        uuidPrefix,
 +    };
 +})((props: { ownerUuid?: string; uuidPrefix: string; uuid?: string } & DispatchProp<any>) =>
 +    resourceShare(props.dispatch, props.uuidPrefix, props.ownerUuid, props.uuid)
 +);
 +
 +// User Resources
 +const renderFirstName = (item: { firstName: string }) => {
 +    return <Typography noWrap>{item.firstName}</Typography>;
 +};
 +
 +export const ResourceFirstName = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<UserResource>(props.uuid)(state.resources);
 +    return resource || { firstName: "" };
 +})(renderFirstName);
 +
 +const renderLastName = (item: { lastName: string }) => <Typography noWrap>{item.lastName}</Typography>;
 +
 +export const ResourceLastName = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<UserResource>(props.uuid)(state.resources);
 +    return resource || { lastName: "" };
 +})(renderLastName);
 +
 +const renderFullName = (dispatch: Dispatch, item: { uuid: string; firstName: string; lastName: string }, link?: boolean) => {
 +    const displayName = (item.firstName + " " + item.lastName).trim() || item.uuid;
 +    return link ? (
 +        <Typography
 +            noWrap
 +            color="primary"
 +            style={{ cursor: "pointer" }}
 +            onClick={() => dispatch<any>(navigateToUserProfile(item.uuid))}
 +        >
 +            {displayName}
 +        </Typography>
 +    ) : (
 +        <Typography noWrap>{displayName}</Typography>
 +    );
 +};
 +
 +export const UserResourceFullName = connect((state: RootState, props: { uuid: string; link?: boolean }) => {
 +    const resource = getResource<UserResource>(props.uuid)(state.resources);
 +    return { item: resource || { uuid: "", firstName: "", lastName: "" }, link: props.link };
 +})((props: { item: { uuid: string; firstName: string; lastName: string }; link?: boolean } & DispatchProp<any>) =>
 +    renderFullName(props.dispatch, props.item, props.link)
 +);
 +
 +const renderUuid = (item: { uuid: string }) => (
 +    <Typography
 +        data-cy="uuid"
 +        noWrap
 +    >
 +        {item.uuid}
 +        {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || "-"}
 +    </Typography>
 +);
 +
 +const renderUuidCopyIcon = (item: { uuid: string }) => (
 +    <Typography
 +        data-cy="uuid"
 +        noWrap
 +    >
 +        {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || "-"}
 +    </Typography>
 +);
 +
 +export const ResourceUuid = connect(
 +    (state: RootState, props: { uuid: string }) => getResource<UserResource>(props.uuid)(state.resources) || { uuid: "" }
 +)(renderUuid);
 +
 +const renderEmail = (item: { email: string }) => <Typography noWrap>{item.email}</Typography>;
 +
 +export const ResourceEmail = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<UserResource>(props.uuid)(state.resources);
 +    return resource || { email: "" };
 +})(renderEmail);
 +
 +enum UserAccountStatus {
 +    ACTIVE = "Active",
 +    INACTIVE = "Inactive",
 +    SETUP = "Setup",
 +    UNKNOWN = "",
 +}
 +
 +const renderAccountStatus = (props: { status: UserAccountStatus }) => (
 +    <Grid
 +        container
 +        alignItems="center"
 +        wrap="nowrap"
 +        spacing={8}
 +        data-cy="account-status"
 +    >
 +        <Grid item>
 +            {(() => {
 +                switch (props.status) {
 +                    case UserAccountStatus.ACTIVE:
 +                        return <ActiveIcon style={{ color: "#4caf50", verticalAlign: "middle" }} />;
 +                    case UserAccountStatus.SETUP:
 +                        return <SetupIcon style={{ color: "#2196f3", verticalAlign: "middle" }} />;
 +                    case UserAccountStatus.INACTIVE:
 +                        return <InactiveIcon style={{ color: "#9e9e9e", verticalAlign: "middle" }} />;
 +                    default:
 +                        return <></>;
 +                }
 +            })()}
 +        </Grid>
 +        <Grid item>
 +            <Typography noWrap>{props.status}</Typography>
 +        </Grid>
 +    </Grid>
 +);
 +
 +const getUserAccountStatus = (state: RootState, props: { uuid: string }) => {
 +    const user = getResource<UserResource>(props.uuid)(state.resources);
 +    // Get membership links for all users group
 +    const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
 +    const permissions = filterResources(
 +        (resource: LinkResource) =>
 +            resource.kind === ResourceKind.LINK &&
 +            resource.linkClass === LinkClass.PERMISSION &&
 +            resource.headUuid === allUsersGroupUuid &&
 +            resource.tailUuid === props.uuid
 +    )(state.resources);
 +
 +    if (user) {
 +        return user.isActive
 +            ? { status: UserAccountStatus.ACTIVE }
 +            : permissions.length > 0
 +            ? { status: UserAccountStatus.SETUP }
 +            : { status: UserAccountStatus.INACTIVE };
 +    } else {
 +        return { status: UserAccountStatus.UNKNOWN };
 +    }
 +};
 +
 +export const ResourceLinkTailAccountStatus = connect((state: RootState, props: { uuid: string }) => {
 +    const link = getResource<LinkResource>(props.uuid)(state.resources);
 +    return link && link.tailKind === ResourceKind.USER ? getUserAccountStatus(state, { uuid: link.tailUuid }) : { status: UserAccountStatus.UNKNOWN };
 +})(renderAccountStatus);
 +
 +export const UserResourceAccountStatus = connect(getUserAccountStatus)(renderAccountStatus);
 +
 +const renderIsHidden = (props: {
 +    memberLinkUuid: string;
 +    permissionLinkUuid: string;
 +    visible: boolean;
 +    canManage: boolean;
 +    setMemberIsHidden: (memberLinkUuid: string, permissionLinkUuid: string, hide: boolean) => void;
 +}) => {
 +    if (props.memberLinkUuid) {
 +        return (
 +            <Checkbox
 +                data-cy="user-visible-checkbox"
 +                color="primary"
 +                checked={props.visible}
 +                disabled={!props.canManage}
 +                onClick={e => {
 +                    e.stopPropagation();
 +                    props.setMemberIsHidden(props.memberLinkUuid, props.permissionLinkUuid, !props.visible);
 +                }}
 +            />
 +        );
 +    } else {
 +        return <Typography />;
 +    }
 +};
 +
 +export const ResourceLinkTailIsVisible = connect(
 +    (state: RootState, props: { uuid: string }) => {
 +        const link = getResource<LinkResource>(props.uuid)(state.resources);
 +        const member = getResource<Resource>(link?.tailUuid || "")(state.resources);
 +        const group = getResource<GroupResource>(link?.headUuid || "")(state.resources);
 +        const permissions = filterResources((resource: LinkResource) => {
 +            return (
 +                resource.linkClass === LinkClass.PERMISSION &&
 +                resource.headUuid === link?.tailUuid &&
 +                resource.tailUuid === group?.uuid &&
 +                resource.name === PermissionLevel.CAN_READ
 +            );
 +        })(state.resources);
 +
 +        const permissionLinkUuid = permissions.length > 0 ? permissions[0].uuid : "";
 +        const isVisible = link && group && permissions.length > 0;
 +        // Consider whether the current user canManage this resurce in addition when it's possible
 +        const isBuiltin = isBuiltinGroup(link?.headUuid || "");
 +
 +        return member?.kind === ResourceKind.USER
 +            ? { memberLinkUuid: link?.uuid, permissionLinkUuid, visible: isVisible, canManage: !isBuiltin }
 +            : { memberLinkUuid: "", permissionLinkUuid: "", visible: false, canManage: false };
 +    },
 +    { setMemberIsHidden }
 +)(renderIsHidden);
 +
 +const renderIsAdmin = (props: { uuid: string; isAdmin: boolean; toggleIsAdmin: (uuid: string) => void }) => (
 +    <Checkbox
 +        color="primary"
 +        checked={props.isAdmin}
 +        onClick={e => {
 +            e.stopPropagation();
 +            props.toggleIsAdmin(props.uuid);
 +        }}
 +    />
 +);
 +
 +export const ResourceIsAdmin = connect(
 +    (state: RootState, props: { uuid: string }) => {
 +        const resource = getResource<UserResource>(props.uuid)(state.resources);
 +        return resource || { isAdmin: false };
 +    },
 +    { toggleIsAdmin }
 +)(renderIsAdmin);
 +
 +const renderUsername = (item: { username: string; uuid: string }) => <Typography noWrap>{item.username || item.uuid}</Typography>;
 +
 +export const ResourceUsername = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<UserResource>(props.uuid)(state.resources);
 +    return resource || { username: "", uuid: props.uuid };
 +})(renderUsername);
 +
 +// Virtual machine resource
 +
 +const renderHostname = (item: { hostname: string }) => <Typography noWrap>{item.hostname}</Typography>;
 +
 +export const VirtualMachineHostname = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<VirtualMachinesResource>(props.uuid)(state.resources);
 +    return resource || { hostname: "" };
 +})(renderHostname);
 +
 +const renderVirtualMachineLogin = (login: { user: string }) => <Typography noWrap>{login.user}</Typography>;
 +
 +export const VirtualMachineLogin = connect((state: RootState, props: { linkUuid: string }) => {
 +    const permission = getResource<LinkResource>(props.linkUuid)(state.resources);
 +    const user = getResource<UserResource>(permission?.tailUuid || "")(state.resources);
 +
 +    return { user: user?.username || permission?.tailUuid || "" };
 +})(renderVirtualMachineLogin);
 +
 +// Common methods
 +const renderCommonData = (data: string) => <Typography noWrap>{data}</Typography>;
 +
 +const renderCommonDate = (date: string) => <Typography noWrap>{formatDate(date)}</Typography>;
 +
 +export const CommonUuid = withResourceData("uuid", renderCommonData);
 +
 +// Api Client Authorizations
 +export const TokenApiClientId = withResourceData("apiClientId", renderCommonData);
 +
 +export const TokenApiToken = withResourceData("apiToken", renderCommonData);
 +
 +export const TokenCreatedByIpAddress = withResourceData("createdByIpAddress", renderCommonDate);
 +
 +export const TokenDefaultOwnerUuid = withResourceData("defaultOwnerUuid", renderCommonData);
 +
 +export const TokenExpiresAt = withResourceData("expiresAt", renderCommonDate);
 +
 +export const TokenLastUsedAt = withResourceData("lastUsedAt", renderCommonDate);
 +
 +export const TokenLastUsedByIpAddress = withResourceData("lastUsedByIpAddress", renderCommonData);
 +
 +export const TokenScopes = withResourceData("scopes", renderCommonData);
 +
 +export const TokenUserId = withResourceData("userId", renderCommonData);
 +
 +const clusterColors = [
 +    ["#f44336", "#fff"],
 +    ["#2196f3", "#fff"],
 +    ["#009688", "#fff"],
 +    ["#cddc39", "#fff"],
 +    ["#ff9800", "#fff"],
 +];
 +
 +export const ResourceCluster = (props: { uuid: string }) => {
 +    const CLUSTER_ID_LENGTH = 5;
 +    const pos = props.uuid.length > CLUSTER_ID_LENGTH ? props.uuid.indexOf("-") : 5;
 +    const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substring(0, pos) : "";
 +    const ci =
 +        pos >= CLUSTER_ID_LENGTH
 +            ? ((props.uuid.charCodeAt(0) * props.uuid.charCodeAt(1) + props.uuid.charCodeAt(2)) * props.uuid.charCodeAt(3) +
 +                  props.uuid.charCodeAt(4)) %
 +              clusterColors.length
 +            : 0;
 +    return (
 +        <span
 +            style={{
 +                backgroundColor: clusterColors[ci][0],
 +                color: clusterColors[ci][1],
 +                padding: "2px 7px",
 +                borderRadius: 3,
 +            }}
 +        >
 +            {clusterId}
 +        </span>
 +    );
 +};
 +
 +// Links Resources
 +const renderLinkName = (item: { name: string }) => <Typography noWrap>{item.name || "-"}</Typography>;
 +
 +export const ResourceLinkName = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<LinkResource>(props.uuid)(state.resources);
 +    return resource || { name: "" };
 +})(renderLinkName);
 +
 +const renderLinkClass = (item: { linkClass: string }) => <Typography noWrap>{item.linkClass}</Typography>;
 +
 +export const ResourceLinkClass = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<LinkResource>(props.uuid)(state.resources);
 +    return resource || { linkClass: "" };
 +})(renderLinkClass);
 +
 +const getResourceDisplayName = (resource: Resource): string => {
 +    if ((resource as UserResource).kind === ResourceKind.USER && typeof (resource as UserResource).firstName !== "undefined") {
 +        // We can be sure the resource is UserResource
 +        return getUserDisplayName(resource as UserResource);
 +    } else {
 +        return (resource as GroupContentsResource).name;
 +    }
 +};
 +
 +const renderResourceLink = (dispatch: Dispatch, item: Resource ) => {
 +    var displayName = getResourceDisplayName(item);
 +
 +    return (
 +        <Typography
 +            noWrap
 +            color="primary"
 +            style={{ cursor: "pointer" }}
 +            onClick={() => {
 +                item.kind === ResourceKind.GROUP && (item as GroupResource).groupClass === "role"
 +                    ? dispatch<any>(navigateToGroupDetails(item.uuid))
 +                    : item.kind === ResourceKind.USER 
 +                    ? dispatch<any>(navigateToUserProfile(item.uuid))
 +                    : dispatch<any>(navigateTo(item.uuid)); 
 +            }}
 +        >
 +            {resourceLabel(item.kind, item && item.kind === ResourceKind.GROUP ? (item as GroupResource).groupClass || "" : "")}:{" "}
 +            {displayName || item.uuid}
 +        </Typography>
 +    );
 +};
 +
 +export const ResourceLinkTail = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<LinkResource>(props.uuid)(state.resources);
 +    const tailResource = getResource<Resource>(resource?.tailUuid || "")(state.resources);
 +
 +    return {
 +        item: tailResource || { uuid: resource?.tailUuid || "", kind: resource?.tailKind || ResourceKind.NONE },
 +    };
 +})((props: { item: Resource } & DispatchProp<any>) => renderResourceLink(props.dispatch, props.item));
 +
 +export const ResourceLinkHead = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<LinkResource>(props.uuid)(state.resources);
 +    const headResource = getResource<Resource>(resource?.headUuid || "")(state.resources);
 +
 +    return {
 +        item: headResource || { uuid: resource?.headUuid || "", kind: resource?.headKind || ResourceKind.NONE },
 +    };
 +})((props: { item: Resource } & DispatchProp<any>) => renderResourceLink(props.dispatch, props.item));
 +
 +export const ResourceLinkUuid = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<LinkResource>(props.uuid)(state.resources);
 +    return resource || { uuid: "" };
 +})(renderUuid);
 +
 +export const ResourceLinkHeadUuid = connect((state: RootState, props: { uuid: string }) => {
 +    const link = getResource<LinkResource>(props.uuid)(state.resources);
 +    const headResource = getResource<Resource>(link?.headUuid || "")(state.resources);
 +
 +    return headResource || { uuid: "" };
 +})(renderUuid);
 +
 +export const ResourceLinkTailUuid = connect((state: RootState, props: { uuid: string }) => {
 +    const link = getResource<LinkResource>(props.uuid)(state.resources);
 +    const tailResource = getResource<Resource>(link?.tailUuid || "")(state.resources);
 +
 +    return tailResource || { uuid: "" };
 +})(renderUuid);
 +
 +const renderLinkDelete = (dispatch: Dispatch, item: LinkResource, canManage: boolean) => {
 +    if (item.uuid) {
 +        return canManage ? (
 +            <Typography noWrap>
 +                <IconButton
 +                    data-cy="resource-delete-button"
 +                    onClick={() => dispatch<any>(openRemoveGroupMemberDialog(item.uuid))}
 +                >
 +                    <RemoveIcon />
 +                </IconButton>
 +            </Typography>
 +        ) : (
 +            <Typography noWrap>
 +                <IconButton
 +                    disabled
 +                    data-cy="resource-delete-button"
 +                >
 +                    <RemoveIcon />
 +                </IconButton>
 +            </Typography>
 +        );
 +    } else {
 +        return <Typography noWrap></Typography>;
 +    }
 +};
 +
 +export const ResourceLinkDelete = connect((state: RootState, props: { uuid: string }) => {
 +    const link = getResource<LinkResource>(props.uuid)(state.resources);
 +    const isBuiltin = isBuiltinGroup(link?.headUuid || "") || isBuiltinGroup(link?.tailUuid || "");
 +
 +    return {
 +        item: link || { uuid: "", kind: ResourceKind.NONE },
 +        canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
 +    };
 +})((props: { item: LinkResource; canManage: boolean } & DispatchProp<any>) => renderLinkDelete(props.dispatch, props.item, props.canManage));
 +
 +export const ResourceLinkTailEmail = connect((state: RootState, props: { uuid: string }) => {
 +    const link = getResource<LinkResource>(props.uuid)(state.resources);
 +    const resource = getResource<UserResource>(link?.tailUuid || "")(state.resources);
 +
 +    return resource || { email: "" };
 +})(renderEmail);
 +
 +export const ResourceLinkTailUsername = connect((state: RootState, props: { uuid: string }) => {
 +    const link = getResource<LinkResource>(props.uuid)(state.resources);
 +    const resource = getResource<UserResource>(link?.tailUuid || "")(state.resources);
 +
 +    return resource || { username: "" };
 +})(renderUsername);
 +
 +const renderPermissionLevel = (dispatch: Dispatch, link: LinkResource, canManage: boolean) => {
 +    return (
 +        <Typography noWrap>
 +            {formatPermissionLevel(link.name as PermissionLevel)}
 +            {canManage ? (
 +                <IconButton
 +                    data-cy="edit-permission-button"
 +                    onClick={event => dispatch<any>(openPermissionEditContextMenu(event, link))}
 +                >
 +                    <RenameIcon />
 +                </IconButton>
 +            ) : (
 +                ""
 +            )}
 +        </Typography>
 +    );
 +};
 +
 +export const ResourceLinkHeadPermissionLevel = connect((state: RootState, props: { uuid: string }) => {
 +    const link = getResource<LinkResource>(props.uuid)(state.resources);
 +    const isBuiltin = isBuiltinGroup(link?.headUuid || "") || isBuiltinGroup(link?.tailUuid || "");
 +
 +    return {
 +        link: link || { uuid: "", name: "", kind: ResourceKind.NONE },
 +        canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
 +    };
 +})((props: { link: LinkResource; canManage: boolean } & DispatchProp<any>) => renderPermissionLevel(props.dispatch, props.link, props.canManage));
 +
 +export const ResourceLinkTailPermissionLevel = connect((state: RootState, props: { uuid: string }) => {
 +    const link = getResource<LinkResource>(props.uuid)(state.resources);
 +    const isBuiltin = isBuiltinGroup(link?.headUuid || "") || isBuiltinGroup(link?.tailUuid || "");
 +
 +    return {
 +        link: link || { uuid: "", name: "", kind: ResourceKind.NONE },
 +        canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
 +    };
 +})((props: { link: LinkResource; canManage: boolean } & DispatchProp<any>) => renderPermissionLevel(props.dispatch, props.link, props.canManage));
 +
 +const getResourceLinkCanManage = (state: RootState, link: LinkResource) => {
 +    const headResource = getResource<Resource>(link.headUuid)(state.resources);
 +    if (headResource && headResource.kind === ResourceKind.GROUP) {
 +        return (headResource as GroupResource).canManage;
 +    } else {
 +        // true for now
 +        return true;
 +    }
 +};
 +
 +// Process Resources
 +const resourceRunProcess = (dispatch: Dispatch, uuid: string) => {
 +    return (
 +        <div>
 +            {uuid && (
 +                <Tooltip title="Run process">
 +                    <IconButton onClick={() => dispatch<any>(openRunProcess(uuid))}>
 +                        <ProcessIcon />
 +                    </IconButton>
 +                </Tooltip>
 +            )}
 +        </div>
 +    );
 +};
 +
 +export const ResourceRunProcess = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
 +    return {
 +        uuid: resource ? resource.uuid : "",
 +    };
 +})((props: { uuid: string } & DispatchProp<any>) => resourceRunProcess(props.dispatch, props.uuid));
 +
 +const renderWorkflowStatus = (uuidPrefix: string, ownerUuid?: string) => {
 +    if (ownerUuid === getPublicUuid(uuidPrefix)) {
 +        return renderStatus(WorkflowStatus.PUBLIC);
 +    } else {
 +        return renderStatus(WorkflowStatus.PRIVATE);
 +    }
 +};
 +
 +const renderStatus = (status: string) => (
 +    <Typography
 +        noWrap
 +        style={{ width: "60px" }}
 +    >
 +        {status}
 +    </Typography>
 +);
 +
 +export const ResourceWorkflowStatus = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
 +    const uuidPrefix = getUuidPrefix(state);
 +    return {
 +        ownerUuid: resource ? resource.ownerUuid : "",
 +        uuidPrefix,
 +    };
 +})((props: { ownerUuid?: string; uuidPrefix: string }) => renderWorkflowStatus(props.uuidPrefix, props.ownerUuid));
 +
 +export const ResourceContainerUuid = connect((state: RootState, props: { uuid: string }) => {
 +    const process = getProcess(props.uuid)(state.resources);
 +    return { uuid: process?.container?.uuid ? process?.container?.uuid : "" };
 +})((props: { uuid: string }) => renderUuid({ uuid: props.uuid }));
 +
 +enum ColumnSelection {
 +    OUTPUT_UUID = "outputUuid",
 +    LOG_UUID = "logUuid",
 +}
 +
 +const renderUuidLinkWithCopyIcon = (dispatch: Dispatch, item: ProcessResource, column: string) => {
 +    const selectedColumnUuid = item[column];
 +    return (
 +        <Grid
 +            container
 +            alignItems="center"
 +            wrap="nowrap"
 +        >
 +            <Grid item>
 +                {selectedColumnUuid ? (
 +                    <Typography
 +                        color="primary"
 +                        style={{ width: "auto", cursor: "pointer" }}
 +                        noWrap
 +                        onClick={() => dispatch<any>(navigateTo(selectedColumnUuid))}
 +                    >
 +                        {selectedColumnUuid}
 +                    </Typography>
 +                ) : (
 +                    "-"
 +                )}
 +            </Grid>
 +            <Grid item>{selectedColumnUuid && renderUuidCopyIcon({ uuid: selectedColumnUuid })}</Grid>
 +        </Grid>
 +    );
 +};
 +
 +export const ResourceOutputUuid = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<ProcessResource>(props.uuid)(state.resources);
 +    return resource;
 +})((process: ProcessResource & DispatchProp<any>) => renderUuidLinkWithCopyIcon(process.dispatch, process, ColumnSelection.OUTPUT_UUID));
 +
 +export const ResourceLogUuid = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<ProcessResource>(props.uuid)(state.resources);
 +    return resource;
 +})((process: ProcessResource & DispatchProp<any>) => renderUuidLinkWithCopyIcon(process.dispatch, process, ColumnSelection.LOG_UUID));
 +
 +export const ResourceParentProcess = connect((state: RootState, props: { uuid: string }) => {
 +    const process = getProcess(props.uuid)(state.resources);
 +    return { parentProcess: process?.containerRequest?.requestingContainerUuid || "" };
 +})((props: { parentProcess: string }) => renderUuid({ uuid: props.parentProcess }));
 +
 +export const ResourceModifiedByUserUuid = connect((state: RootState, props: { uuid: string }) => {
 +    const process = getProcess(props.uuid)(state.resources);
 +    return { userUuid: process?.containerRequest?.modifiedByUserUuid || "" };
 +})((props: { userUuid: string }) => renderUuid({ uuid: props.userUuid }));
 +
 +export const ResourceCreatedAtDate = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
 +    return { date: resource ? resource.createdAt : "" };
 +})((props: { date: string }) => renderDate(props.date));
 +
 +export const ResourceLastModifiedDate = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
 +    return { date: resource ? resource.modifiedAt : "" };
 +})((props: { date: string }) => renderDate(props.date));
 +
 +export const ResourceTrashDate = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<TrashableResource>(props.uuid)(state.resources);
 +    return { date: resource ? resource.trashAt : "" };
 +})((props: { date: string }) => renderDate(props.date));
 +
 +export const ResourceDeleteDate = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<TrashableResource>(props.uuid)(state.resources);
 +    return { date: resource ? resource.deleteAt : "" };
 +})((props: { date: string }) => renderDate(props.date));
 +
 +export const renderFileSize = (fileSize?: number) => (
 +    <Typography
 +        noWrap
 +        style={{ minWidth: "45px" }}
 +    >
 +        {formatFileSize(fileSize)}
 +    </Typography>
 +);
 +
 +export const ResourceFileSize = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<CollectionResource>(props.uuid)(state.resources);
 +
 +    if (resource && resource.kind !== ResourceKind.COLLECTION) {
 +        return { fileSize: "" };
 +    }
 +
 +    return { fileSize: resource ? resource.fileSizeTotal : 0 };
 +})((props: { fileSize?: number }) => renderFileSize(props.fileSize));
 +
 +const renderOwner = (owner: string) => <Typography noWrap>{owner || "-"}</Typography>;
 +
 +export const ResourceOwner = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
 +    return { owner: resource ? resource.ownerUuid : "" };
 +})((props: { owner: string }) => renderOwner(props.owner));
 +
 +export const ResourceOwnerName = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
 +    const ownerNameState = state.ownerName;
 +    const ownerName = ownerNameState.find(it => it.uuid === resource!.ownerUuid);
 +    return { owner: ownerName ? ownerName!.name : resource!.ownerUuid };
 +})((props: { owner: string }) => renderOwner(props.owner));
 +
 +export const ResourceUUID = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<CollectionResource>(props.uuid)(state.resources);
 +    return { uuid: resource ? resource.uuid : "" };
 +})((props: { uuid: string }) => renderUuid({ uuid: props.uuid }));
 +
 +const renderVersion = (version: number) => {
 +    return <Typography>{version ?? "-"}</Typography>;
 +};
 +
 +export const ResourceVersion = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<CollectionResource>(props.uuid)(state.resources);
 +    return { version: resource ? resource.version : "" };
 +})((props: { version: number }) => renderVersion(props.version));
 +
 +const renderPortableDataHash = (portableDataHash: string | null) => (
 +    <Typography noWrap>
 +        {portableDataHash ? (
 +            <>
 +                {portableDataHash}
 +                <CopyToClipboardSnackbar value={portableDataHash} />
 +            </>
 +        ) : (
 +            "-"
 +        )}
 +    </Typography>
 +);
 +
 +export const ResourcePortableDataHash = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<CollectionResource>(props.uuid)(state.resources);
 +    return { portableDataHash: resource ? resource.portableDataHash : "" };
 +})((props: { portableDataHash: string }) => renderPortableDataHash(props.portableDataHash));
 +
 +const renderFileCount = (fileCount: number) => {
 +    return <Typography>{fileCount ?? "-"}</Typography>;
 +};
 +
 +export const ResourceFileCount = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<CollectionResource>(props.uuid)(state.resources);
 +    return { fileCount: resource ? resource.fileCount : "" };
 +})((props: { fileCount: number }) => renderFileCount(props.fileCount));
 +
 +const userFromID = connect((state: RootState, props: { uuid: string }) => {
 +    let userFullname = "";
 +    const resource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
 +
 +    if (resource) {
 +        userFullname = getUserFullname(resource as User) || (resource as GroupContentsResource).name;
 +    }
 +
 +    return { uuid: props.uuid, userFullname };
 +});
 +
 +const ownerFromResourceId = compose(
 +    connect((state: RootState, props: { uuid: string }) => {
 +        const childResource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
 +        return { uuid: childResource ? (childResource as Resource).ownerUuid : "" };
 +    }),
 +    userFromID
 +);
 +
 +const _resourceWithName = withStyles(
 +    {},
 +    { withTheme: true }
 +)((props: { uuid: string; userFullname: string; dispatch: Dispatch; theme: ArvadosTheme }) => {
 +    const { uuid, userFullname, dispatch, theme } = props;
 +    if (userFullname === "") {
 +        dispatch<any>(loadResource(uuid, false));
 +        return (
 +            <Typography
 +                style={{ color: theme.palette.primary.main }}
 +                inline
 +                noWrap
 +            >
 +                {uuid}
 +            </Typography>
 +        );
 +    }
 +
 +    return (
 +        <Typography
 +            style={{ color: theme.palette.primary.main }}
 +            inline
 +            noWrap
 +        >
 +            {userFullname} ({uuid})
 +        </Typography>
 +    );
 +});
 +
 +const _resourceWithNameLink = withStyles(
 +    {},
 +    { withTheme: true }
 +)((props: { uuid: string; userFullname: string; dispatch: Dispatch; theme: ArvadosTheme }) => {
 +    const { uuid, userFullname, dispatch, theme } = props;
 +    if (!userFullname) {
 +        dispatch<any>(loadResource(uuid, false));
 +    }
 +
 +    return (
 +        <Typography
 +            style={{ color: theme.palette.primary.main, cursor: 'pointer' }}
 +            inline
 +            noWrap
 +            onClick={() => dispatch<any>(navigateTo(uuid))}
 +        >
 +            {userFullname ? userFullname : uuid}
 +        </Typography>
 +    )
 +});
 +
 +
 +export const ResourceOwnerWithNameLink = ownerFromResourceId(_resourceWithNameLink);
 +
 +export const ResourceOwnerWithName = ownerFromResourceId(_resourceWithName);
 +
 +export const ResourceWithName = userFromID(_resourceWithName);
 +
 +export const UserNameFromID = compose(userFromID)((props: { uuid: string; displayAsText?: string; userFullname: string; dispatch: Dispatch }) => {
 +    const { uuid, userFullname, dispatch } = props;
 +
 +    if (userFullname === "") {
 +        dispatch<any>(loadResource(uuid, false));
 +    }
 +    return <span>{userFullname ? userFullname : uuid}</span>;
 +});
 +
 +export const ResponsiblePerson = compose(
 +    connect((state: RootState, props: { uuid: string; parentRef: HTMLElement | null }) => {
 +        let responsiblePersonName: string = "";
 +        let responsiblePersonUUID: string = "";
 +        let responsiblePersonProperty: string = "";
 +
 +        if (state.auth.config.clusterConfig.Collections.ManagedProperties) {
 +            let index = 0;
 +            const keys = Object.keys(state.auth.config.clusterConfig.Collections.ManagedProperties);
 +
 +            while (!responsiblePersonProperty && keys[index]) {
 +                const key = keys[index];
 +                if (state.auth.config.clusterConfig.Collections.ManagedProperties[key].Function === "original_owner") {
 +                    responsiblePersonProperty = key;
 +                }
 +                index++;
 +            }
 +        }
 +
 +        let resource: Resource | undefined = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
 +
 +        while (resource && resource.kind !== ResourceKind.USER && responsiblePersonProperty) {
 +            responsiblePersonUUID = (resource as CollectionResource).properties[responsiblePersonProperty];
 +            resource = getResource<GroupContentsResource & UserResource>(responsiblePersonUUID)(state.resources);
 +        }
 +
 +        if (resource && resource.kind === ResourceKind.USER) {
 +            responsiblePersonName = getUserFullname(resource as UserResource) || (resource as GroupContentsResource).name;
 +        }
 +
 +        return { uuid: responsiblePersonUUID, responsiblePersonName, parentRef: props.parentRef };
 +    }),
 +    withStyles({}, { withTheme: true })
 +)((props: { uuid: string | null; responsiblePersonName: string; parentRef: HTMLElement | null; theme: ArvadosTheme }) => {
 +    const { uuid, responsiblePersonName, parentRef, theme } = props;
 +
 +    if (!uuid && parentRef) {
 +        parentRef.style.display = "none";
 +        return null;
 +    } else if (parentRef) {
 +        parentRef.style.display = "block";
 +    }
 +
 +    if (!responsiblePersonName) {
 +        return (
 +            <Typography
 +                style={{ color: theme.palette.primary.main }}
 +                inline
 +                noWrap
 +            >
 +                {uuid}
 +            </Typography>
 +        );
 +    }
 +
 +    return (
 +        <Typography
 +            style={{ color: theme.palette.primary.main }}
 +            inline
 +            noWrap
 +        >
 +            {responsiblePersonName} ({uuid})
 +        </Typography>
 +    );
 +});
 +
 +const renderType = (type: string, subtype: string) => <Typography noWrap>{resourceLabel(type, subtype)}</Typography>;
 +
 +export const ResourceType = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
 +    return { type: resource ? resource.kind : "", subtype: resource && resource.kind === ResourceKind.GROUP ? resource.groupClass : "" };
 +})((props: { type: string; subtype: string }) => renderType(props.type, props.subtype));
 +
 +export const ResourceStatus = connect((state: RootState, props: { uuid: string }) => {
 +    return { resource: getResource<GroupContentsResource>(props.uuid)(state.resources) };
 +})((props: { resource: GroupContentsResource }) =>
 +    props.resource && props.resource.kind === ResourceKind.COLLECTION ? (
 +        <CollectionStatus uuid={props.resource.uuid} />
 +    ) : (
 +        <ProcessStatus uuid={props.resource.uuid} />
 +    )
 +);
 +
 +export const CollectionStatus = connect((state: RootState, props: { uuid: string }) => {
 +    return { collection: getResource<CollectionResource>(props.uuid)(state.resources) };
 +})((props: { collection: CollectionResource }) =>
 +    props.collection.uuid !== props.collection.currentVersionUuid ? (
 +        <Typography>version {props.collection.version}</Typography>
 +    ) : (
 +        <Typography>head version</Typography>
 +    )
 +);
 +
 +export const CollectionName = connect((state: RootState, props: { uuid: string; className?: string }) => {
 +    return {
 +        collection: getResource<CollectionResource>(props.uuid)(state.resources),
 +        uuid: props.uuid,
 +        className: props.className,
 +    };
 +})((props: { collection: CollectionResource; uuid: string; className?: string }) => (
 +    <Typography className={props.className}>{props.collection?.name || props.uuid}</Typography>
 +));
 +
 +export const ProcessStatus = compose(
 +    connect((state: RootState, props: { uuid: string }) => {
 +        return { process: getProcess(props.uuid)(state.resources) };
 +    }),
 +    withStyles({}, { withTheme: true })
 +)((props: { process?: Process; theme: ArvadosTheme }) =>
 +    props.process ? (
 +        <Chip
 +            label={getProcessStatus(props.process)}
 +            style={{
 +                height: props.theme.spacing.unit * 3,
 +                width: props.theme.spacing.unit * 12,
 +                ...getProcessStatusStyles(getProcessStatus(props.process), props.theme),
 +                fontSize: "0.875rem",
 +                borderRadius: props.theme.spacing.unit * 0.625,
 +            }}
 +        />
 +    ) : (
 +        <Typography>-</Typography>
 +    )
 +);
 +
 +export const ProcessStartDate = connect((state: RootState, props: { uuid: string }) => {
 +    const process = getProcess(props.uuid)(state.resources);
 +    return { date: process && process.container ? process.container.startedAt : "" };
 +})((props: { date: string }) => renderDate(props.date));
 +
 +export const renderRunTime = (time: number) => (
 +    <Typography
 +        noWrap
 +        style={{ minWidth: "45px" }}
 +    >
 +        {formatTime(time, true)}
 +    </Typography>
 +);
 +
 +interface ContainerRunTimeProps {
 +    process: Process;
 +}
 +
 +interface ContainerRunTimeState {
 +    runtime: number;
 +}
 +
 +export const ContainerRunTime = connect((state: RootState, props: { uuid: string }) => {
 +    return { process: getProcess(props.uuid)(state.resources) };
 +})(
 +    class extends React.Component<ContainerRunTimeProps, ContainerRunTimeState> {
 +        private timer: any;
 +
 +        constructor(props: ContainerRunTimeProps) {
 +            super(props);
 +            this.state = { runtime: this.getRuntime() };
 +        }
 +
 +        getRuntime() {
 +            return this.props.process ? getProcessRuntime(this.props.process) : 0;
 +        }
 +
 +        updateRuntime() {
 +            this.setState({ runtime: this.getRuntime() });
 +        }
 +
 +        componentDidMount() {
 +            this.timer = setInterval(this.updateRuntime.bind(this), 5000);
 +        }
 +
 +        componentWillUnmount() {
 +            clearInterval(this.timer);
 +        }
 +
 +        render() {
 +            return this.props.process ? renderRunTime(this.state.runtime) : <Typography>-</Typography>;
 +        }
 +    }
 +);
index e9175f57ba423e5069064db69876ddef15c97e1c,2653a2103345fe40a99cf9deb467e162efb05572..2653a2103345fe40a99cf9deb467e162efb05572
@@@ -83,8 -83,11 +83,11 @@@ const getItem = (res: DetailsResource)
      }
  };
  
- const mapStateToProps = ({ auth, detailsPanel, resources, collectionPanelFiles }: RootState) => {
-     const resource = getResource(detailsPanel.resourceUuid)(resources) as DetailsResource | undefined;
+ const mapStateToProps = ({ auth, detailsPanel, resources, collectionPanelFiles, multiselect, router }: RootState) => {
+     const isDetailsResourceChecked = multiselect.checkedList[detailsPanel.resourceUuid]
+     const currentRoute = router.location ? router.location.pathname : "";
+     const currentItemUuid = isDetailsResourceChecked || currentRoute.includes('collections') ? detailsPanel.resourceUuid : multiselect.selectedUuid ? multiselect.selectedUuid : currentRoute.split('/')[2];
+     const resource = getResource(currentItemUuid)(resources) as DetailsResource | undefined;
      const file = resource
          ? undefined
          : getNode(detailsPanel.resourceUuid)(collectionPanelFiles);
index b0a2a1b024793aafc8e86d269f468c7a49324862,a8a8f45748276dfd6a4578d6c54de7534c0ba13b..a8a8f45748276dfd6a4578d6c54de7534c0ba13b
@@@ -2,37 -2,93 +2,93 @@@
  //
  // SPDX-License-Identifier: AGPL-3.0
  
- import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
- import { MoveToIcon, CopyIcon } from "components/icon/icon";
+ import { MoveToIcon, CopyIcon, RenameIcon } from "components/icon/icon";
  import { openMoveCollectionDialog } from "store/collections/collection-move-actions";
  import { openCollectionCopyDialog, openMultiCollectionCopyDialog } from "store/collections/collection-copy-actions";
- import { ToggleTrashAction } from "views-components/context-menu/actions/trash-action";
  import { toggleCollectionTrashed } from "store/trash/trash-actions";
  import { ContextMenuResource } from "store/context-menu/context-menu-actions";
+ import { msCommonActionSet, MultiSelectMenuActionSet, MultiSelectMenuAction } from "./ms-menu-actions";
+ import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+ import { TrashIcon, Link, FolderSharedIcon } from "components/icon/icon";
+ import { openCollectionUpdateDialog } from "store/collections/collection-update-actions";
+ import { copyToClipboardAction } from "store/open-in-new-tab/open-in-new-tab.actions";
+ import { openWebDavS3InfoDialog } from "store/collections/collection-info-actions";
  
- export const msCollectionActionSet: ContextMenuActionSet = [
+ const { MAKE_A_COPY, MOVE_TO, MOVE_TO_TRASH, EDIT_COLLECTION, OPEN_IN_NEW_TAB, OPEN_W_3RD_PARTY_CLIENT, COPY_TO_CLIPBOARD, VIEW_DETAILS, API_DETAILS, ADD_TO_FAVORITES, SHARE} = MultiSelectMenuActionNames;
+ const msCopyCollection: MultiSelectMenuAction = {
+     name: MAKE_A_COPY,
+     icon: CopyIcon,
+     hasAlts: false,
+     isForMulti: true,
+     execute: (dispatch, [...resources]) => {
+         if (resources[0].fromContextMenu || resources.length === 1) dispatch<any>(openCollectionCopyDialog(resources[0]));
+         else dispatch<any>(openMultiCollectionCopyDialog(resources[0]));
+     },
+ }
+ const msMoveCollection: MultiSelectMenuAction = {
+     name: MOVE_TO,
+     icon: MoveToIcon,
+     hasAlts: false,
+     isForMulti: true,
+     execute: (dispatch, resources) => dispatch<any>(openMoveCollectionDialog(resources[0])),
+ }
+ const msToggleTrashAction: MultiSelectMenuAction = {
+     name: MOVE_TO_TRASH,
+     icon: TrashIcon,
+     isForMulti: true,
+     hasAlts: false,
+     execute: (dispatch, resources: ContextMenuResource[]) => {
+         for (const resource of [...resources]) {
+             dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
+         }
+     },
+ }
+ const msEditCollection: MultiSelectMenuAction = {
+     name: MultiSelectMenuActionNames.EDIT_COLLECTION,
+     icon: RenameIcon,
+     hasAlts: false,
+     isForMulti: false,
+     execute: (dispatch, resources) => {
+         dispatch<any>(openCollectionUpdateDialog(resources[0]));
+     },
+ }
+ const msCopyToClipboardMenuAction: MultiSelectMenuAction  = {
+     name: COPY_TO_CLIPBOARD,
+     icon: Link,
+     hasAlts: false,
+     isForMulti: false,
+     execute: (dispatch, resources) => {
+         dispatch<any>(copyToClipboardAction(resources));
+     },
+ };
+ const msOpenWith3rdPartyClientAction: MultiSelectMenuAction  = {
+     name: OPEN_W_3RD_PARTY_CLIENT,
+     icon: FolderSharedIcon,
+     hasAlts: false,
+     isForMulti: false,
+     execute: (dispatch, resources) => {
+         dispatch<any>(openWebDavS3InfoDialog(resources[0].uuid));
+     },
+ };
+ export const msCollectionActionSet: MultiSelectMenuActionSet = [
      [
-         {
-             icon: CopyIcon,
-             name: "Make a copy",
-             execute: (dispatch, [...resources]) => {
-                 if (resources[0].fromContextMenu || resources.length === 1) dispatch<any>(openCollectionCopyDialog(resources[0]));
-                 else dispatch<any>(openMultiCollectionCopyDialog(resources[0]));
-             },
-         },
-         {
-             icon: MoveToIcon,
-             name: "Move to",
-             execute: (dispatch, resources) => dispatch<any>(openMoveCollectionDialog(resources[0])),
-         },
-         {
-             component: ToggleTrashAction,
-             name: "ToggleTrashAction",
-             execute: (dispatch, resources: ContextMenuResource[]) => {
-                 for (const resource of [...resources]) {
-                     dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
-                 }
-             },
-         },
+         ...msCommonActionSet,
+         msCopyCollection,
+         msMoveCollection,
+         msToggleTrashAction,
+         msEditCollection,
+         msCopyToClipboardMenuAction,
+         msOpenWith3rdPartyClientAction
      ],
  ];
+ export const msReadOnlyCollectionActionFilter = new Set([OPEN_IN_NEW_TAB, COPY_TO_CLIPBOARD, MAKE_A_COPY, VIEW_DETAILS, API_DETAILS, ADD_TO_FAVORITES, OPEN_W_3RD_PARTY_CLIENT]);
+ export const msCommonCollectionActionFilter = new Set([OPEN_IN_NEW_TAB, COPY_TO_CLIPBOARD, MAKE_A_COPY, VIEW_DETAILS, API_DETAILS, OPEN_W_3RD_PARTY_CLIENT, EDIT_COLLECTION, SHARE, MOVE_TO, ADD_TO_FAVORITES, MOVE_TO_TRASH])
+ export const msOldCollectionActionFilter = new Set([OPEN_IN_NEW_TAB, COPY_TO_CLIPBOARD, MAKE_A_COPY, VIEW_DETAILS, API_DETAILS, OPEN_W_3RD_PARTY_CLIENT, EDIT_COLLECTION, SHARE, MOVE_TO, ADD_TO_FAVORITES, MOVE_TO_TRASH])
index 0000000000000000000000000000000000000000,91e96d9bfbe002304616782d120f8239850150a4..91e96d9bfbe002304616782d120f8239850150a4
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,144 +1,144 @@@
+ // Copyright (C) The Arvados Authors. All rights reserved.
+ //
+ // SPDX-License-Identifier: AGPL-3.0
+ import { Dispatch } from 'redux';
+ import { IconType } from 'components/icon/icon';
+ import { ResourcesState } from 'store/resources/resources';
+ import { FavoritesState } from 'store/favorites/favorites-reducer';
+ import { ContextMenuResource } from 'store/context-menu/context-menu-actions';
+ import { AddFavoriteIcon, AdvancedIcon, DetailsIcon, OpenIcon, PublicFavoriteIcon, RemoveFavoriteIcon, ShareIcon } from 'components/icon/icon';
+ import { checkFavorite } from 'store/favorites/favorites-reducer';
+ import { toggleFavorite } from 'store/favorites/favorites-actions';
+ import { favoritePanelActions } from 'store/favorite-panel/favorite-panel-action';
+ import { openInNewTabAction } from 'store/open-in-new-tab/open-in-new-tab.actions';
+ import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
+ import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
+ import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions';
+ import { togglePublicFavorite } from "store/public-favorites/public-favorites-actions";
+ import { publicFavoritePanelActions } from "store/public-favorites-panel/public-favorites-action";
+ import { PublicFavoritesState } from 'store/public-favorites/public-favorites-reducer';
+ export enum MultiSelectMenuActionNames {
+     ADD_TO_FAVORITES = 'Add to Favorites',
+     MOVE_TO_TRASH = 'Move to trash',
+     ADD_TO_PUBLIC_FAVORITES = 'Add to public favorites',
+     API_DETAILS = 'API Details',
+     CANCEL = 'CANCEL',
+     COPY_AND_RERUN_PROCESS = 'Copy and re-run process',
+     COPY_TO_CLIPBOARD = 'Copy to clipboard',
+     DELETE_WORKFLOW = 'Delete Workflow',
+     EDIT_COLLECTION = 'Edit collection',
+     EDIT_PROJECT = 'Edit project',
+     EDIT_PROCESS = 'Edit process',
+     FREEZE_PROJECT = 'Freeze Project',
+     MAKE_A_COPY = 'Make a copy',
+     MOVE_TO = 'Move to',
+     NEW_PROJECT = 'New project',
+     OPEN_IN_NEW_TAB = 'Open in new tab',
+     OPEN_W_3RD_PARTY_CLIENT = 'Open with 3rd party client',
+     OUTPUTS = 'Outputs',
+     REMOVE = 'Remove',
+     RUN_WORKFLOW = 'Run Workflow',
+     SHARE = 'Share',
+     VIEW_DETAILS = 'View details',
+ };
+ export type MultiSelectMenuAction = {
+     name: string;
+     icon: IconType;
+     hasAlts: boolean;
+     altName?: string;
+     altIcon?: IconType;
+     isForMulti: boolean;
+     useAlts?: (uuid: string | null, iconProps: {resources: ResourcesState, favorites: FavoritesState, publicFavorites: PublicFavoritesState}) => boolean;
+     execute(dispatch: Dispatch, resources: ContextMenuResource[], state?: any): void;
+     adminOnly?: boolean;
+ };
+ export type MultiSelectMenuActionSet = MultiSelectMenuAction[][];
+ const { ADD_TO_FAVORITES, ADD_TO_PUBLIC_FAVORITES, OPEN_IN_NEW_TAB, VIEW_DETAILS, API_DETAILS, SHARE } = MultiSelectMenuActionNames;
+ const msToggleFavoriteAction: MultiSelectMenuAction = {
+     name: ADD_TO_FAVORITES,
+     icon: AddFavoriteIcon,
+     hasAlts: true,
+     altName: 'Remove from Favorites',
+     altIcon: RemoveFavoriteIcon,
+     isForMulti: false,
+     useAlts: (uuid: string, iconProps) => {
+         return checkFavorite(uuid, iconProps.favorites);
+     },
+     execute: (dispatch, resources) => {
+         dispatch<any>(toggleFavorite(resources[0])).then(() => {
+             dispatch(favoritePanelActions.REQUEST_ITEMS());
+         });
+     },
+ };
+ const msOpenInNewTabMenuAction: MultiSelectMenuAction  = {
+     name: OPEN_IN_NEW_TAB,
+     icon: OpenIcon,
+     hasAlts: false,
+     isForMulti: false,
+     execute: (dispatch, resources) => {
+         dispatch<any>(openInNewTabAction(resources[0]));
+     },
+ };
+ const msViewDetailsAction: MultiSelectMenuAction  = {
+     name: VIEW_DETAILS,
+     icon: DetailsIcon,
+     hasAlts: false,
+     isForMulti: false,
+     execute: (dispatch) => {
+         dispatch<any>(toggleDetailsPanel());
+     },
+ };
+ const msAdvancedAction: MultiSelectMenuAction  = {
+     name: API_DETAILS,
+     icon: AdvancedIcon,
+     hasAlts: false,
+     isForMulti: false,
+     execute: (dispatch, resources) => {
+         dispatch<any>(openAdvancedTabDialog(resources[0].uuid));
+     },
+ };
+ const msShareAction: MultiSelectMenuAction  = {
+     name: SHARE,
+     icon: ShareIcon,
+     hasAlts: false,
+     isForMulti: false,
+     execute: (dispatch, resources) => {
+         dispatch<any>(openSharingDialog(resources[0].uuid));
+     },
+ };
+ const msTogglePublicFavoriteAction: MultiSelectMenuAction = {
+     name: ADD_TO_PUBLIC_FAVORITES,
+     icon: PublicFavoriteIcon,
+     hasAlts: true,
+     altName: 'Remove from public favorites',
+     altIcon: PublicFavoriteIcon,
+     isForMulti: false,
+     useAlts: (uuid: string, iconProps) => {
+         return iconProps.publicFavorites[uuid] === true
+     },
+     execute: (dispatch, resources) => {
+         dispatch<any>(togglePublicFavorite(resources[0])).then(() => {
+             dispatch(publicFavoritePanelActions.REQUEST_ITEMS());
+         });
+     },
+ };
+ export const msCommonActionSet = [
+     msToggleFavoriteAction,
+     msOpenInNewTabMenuAction,
+     msViewDetailsAction,
+     msAdvancedAction,
+     msShareAction,
+     msTogglePublicFavoriteAction
+ ];
index 820fc7999eeb0595c22fba64a7f64cccfd8e61f0,7802ad81f12cb303948c8dc7de82655478f21527..7802ad81f12cb303948c8dc7de82655478f21527
@@@ -2,36 -2,97 +2,97 @@@
  //
  // SPDX-License-Identifier: AGPL-3.0
  
- import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
- import { MoveToIcon, RemoveIcon, ReRunProcessIcon } from "components/icon/icon";
+ import { MoveToIcon, RemoveIcon, ReRunProcessIcon, OutputIcon, RenameIcon, StopIcon } from "components/icon/icon";
  import { openMoveProcessDialog } from "store/processes/process-move-actions";
  import { openCopyProcessDialog } from "store/processes/process-copy-actions";
  import { openRemoveProcessDialog } from "store/processes/processes-actions";
+ import { MultiSelectMenuAction, MultiSelectMenuActionSet, msCommonActionSet } from "./ms-menu-actions";
+ import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+ import { openProcessUpdateDialog } from "store/processes/process-update-actions";
+ import { msNavigateToOutput } from "store/multiselect/multiselect-actions";
+ import { cancelRunningWorkflow } from "store/processes/processes-actions";
  
- export const msProcessActionSet: ContextMenuActionSet = [
+ const msCopyAndRerunProcess: MultiSelectMenuAction = {
+     name: MultiSelectMenuActionNames.COPY_AND_RERUN_PROCESS,
+     icon: ReRunProcessIcon,
+     hasAlts: false,
+     isForMulti: false,
+     execute: (dispatch, resources) => {
+         for (const resource of [...resources]) {
+             dispatch<any>(openCopyProcessDialog(resource));
+         }
+     },
+ }
+ const msRemoveProcess: MultiSelectMenuAction = {
+     name: MultiSelectMenuActionNames.REMOVE,
+     icon: RemoveIcon,
+     hasAlts: false,
+     isForMulti: true,
+     execute: (dispatch, resources) => {
+         dispatch<any>(openRemoveProcessDialog(resources[0], resources.length));
+     },
+ }
+ const msMoveTo: MultiSelectMenuAction = {
+     name: MultiSelectMenuActionNames.MOVE_TO,
+     icon: MoveToIcon,
+     hasAlts: false,
+     isForMulti: true,
+     execute: (dispatch, resources) => {
+         dispatch<any>(openMoveProcessDialog(resources[0]));
+     },
+ }
+ const msViewOutputs: MultiSelectMenuAction = {
+     name: MultiSelectMenuActionNames.OUTPUTS,
+     icon: OutputIcon,
+     hasAlts: false,
+     isForMulti: false,
+     execute: (dispatch, resources) => {
+                 if (resources[0]) {
+             dispatch<any>(msNavigateToOutput(resources[0]));
+         }
+     },
+ }
+ const msEditProcess: MultiSelectMenuAction = {
+     name: MultiSelectMenuActionNames.EDIT_PROCESS,
+     icon: RenameIcon,
+     hasAlts: false,
+     isForMulti: false,
+     execute: (dispatch, resources) => {
+         dispatch<any>(openProcessUpdateDialog(resources[0]));
+     },
+ }
+ const msCancelProcess: MultiSelectMenuAction = {
+     name: MultiSelectMenuActionNames.CANCEL,
+     icon: StopIcon,
+     hasAlts: false,
+     isForMulti: false,
+     execute: (dispatch, resources) => {
+         dispatch<any>(cancelRunningWorkflow(resources[0].uuid));
+     },
+ }
+ export const msProcessActionSet: MultiSelectMenuActionSet = [
      [
-         {
-             icon: ReRunProcessIcon,
-             name: "Copy and re-run process",
-             execute: (dispatch, resources) => {
-                 for (const resource of [...resources]) {
-                     dispatch<any>(openCopyProcessDialog(resource));
-                 }
-             },
-         },
-         {
-             icon: MoveToIcon,
-             name: "Move to",
-             execute: (dispatch, resources) => {
-                 dispatch<any>(openMoveProcessDialog(resources[0]));
-             },
-         },
-         {
-             name: "Remove",
-             icon: RemoveIcon,
-             execute: (dispatch, resources) => {
-                 dispatch<any>(openRemoveProcessDialog(resources[0], resources.length));
-             },
-         },
-     ],
+         ...msCommonActionSet,
+         msCopyAndRerunProcess,
+         msRemoveProcess,
+         msMoveTo,
+         msViewOutputs,
+         msEditProcess,
+         msCancelProcess
+     ]
  ];
+ const { MOVE_TO, REMOVE, COPY_AND_RERUN_PROCESS, ADD_TO_FAVORITES, OPEN_IN_NEW_TAB, VIEW_DETAILS, API_DETAILS, SHARE, ADD_TO_PUBLIC_FAVORITES, OUTPUTS, EDIT_PROCESS, CANCEL } = MultiSelectMenuActionNames
+ export const msCommonProcessActionFilter = new Set([MOVE_TO, REMOVE, COPY_AND_RERUN_PROCESS, ADD_TO_FAVORITES, OPEN_IN_NEW_TAB, VIEW_DETAILS, API_DETAILS, SHARE, OUTPUTS, EDIT_PROCESS ]);
+ export const msRunningProcessActionFilter = new Set([MOVE_TO, REMOVE, COPY_AND_RERUN_PROCESS, ADD_TO_FAVORITES, OPEN_IN_NEW_TAB, VIEW_DETAILS, API_DETAILS, SHARE, OUTPUTS, EDIT_PROCESS, CANCEL ]);
+ export const msReadOnlyProcessActionFilter = new Set([COPY_AND_RERUN_PROCESS, ADD_TO_FAVORITES, OPEN_IN_NEW_TAB, VIEW_DETAILS, API_DETAILS, OUTPUTS ]);
+ export const msAdminProcessActionFilter = new Set([MOVE_TO, REMOVE, COPY_AND_RERUN_PROCESS, ADD_TO_FAVORITES, OPEN_IN_NEW_TAB, VIEW_DETAILS, API_DETAILS, SHARE, ADD_TO_PUBLIC_FAVORITES, OUTPUTS, EDIT_PROCESS ]);
index 2b5dfa22a5d9a0c16fd1f8fdf07534c91c87ce6f,ee1ea1d1792911ad850791caba0789122441d5fd..ee1ea1d1792911ad850791caba0789122441d5fd
  //
  // SPDX-License-Identifier: AGPL-3.0
  
- import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
- import { MoveToIcon, Link } from "components/icon/icon";
- import { openMoveProjectDialog } from "store/projects/project-move-actions";
- import { ToggleTrashAction } from "views-components/context-menu/actions/trash-action";
- import { toggleProjectTrashed } from "store/trash/trash-actions";
- import { copyToClipboardAction } from "store/open-in-new-tab/open-in-new-tab.actions";
- export const msCopyToClipboardMenuAction = {
+ import { MultiSelectMenuAction, MultiSelectMenuActionSet, msCommonActionSet } from 'views-components/multiselect-toolbar/ms-menu-actions';
+ import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+ import { openMoveProjectDialog } from 'store/projects/project-move-actions';
+ import { toggleProjectTrashed } from 'store/trash/trash-actions';
+ import {
+     FreezeIcon,
+     MoveToIcon,
+     NewProjectIcon,
+     RenameIcon,
+     UnfreezeIcon,
+ } from 'components/icon/icon';
+ import { RestoreFromTrashIcon, TrashIcon, FolderSharedIcon, Link } from 'components/icon/icon';
+ import { getResource } from 'store/resources/resources';
+ import { openProjectCreateDialog } from 'store/projects/project-create-actions';
+ import { openProjectUpdateDialog } from 'store/projects/project-update-actions';
+ import { freezeProject, unfreezeProject } from 'store/projects/project-lock-actions';
+ import { openWebDavS3InfoDialog } from 'store/collections/collection-info-actions';
+ import { copyToClipboardAction } from 'store/open-in-new-tab/open-in-new-tab.actions';
+ const {
+     ADD_TO_FAVORITES,
+     ADD_TO_PUBLIC_FAVORITES,
+     OPEN_IN_NEW_TAB,
+     COPY_TO_CLIPBOARD,
+     VIEW_DETAILS,
+     API_DETAILS,
+     OPEN_W_3RD_PARTY_CLIENT,
+     EDIT_PROJECT,
+     SHARE,
+     MOVE_TO,
+     MOVE_TO_TRASH,
+     FREEZE_PROJECT,
+     NEW_PROJECT,
+ } = MultiSelectMenuActionNames;
+ const msCopyToClipboardMenuAction: MultiSelectMenuAction  = {
+     name: COPY_TO_CLIPBOARD,
      icon: Link,
-     name: "Copy to clipboard",
+     hasAlts: false,
+     isForMulti: false,
+     execute: (dispatch, resources) => {
+         dispatch<any>(copyToClipboardAction(resources));
+     },
+ };
+ const msEditProjectAction: MultiSelectMenuAction = {
+     name: EDIT_PROJECT,
+     icon: RenameIcon,
+     hasAlts: false,
+     isForMulti: false,
      execute: (dispatch, resources) => {
-         dispatch(copyToClipboardAction(resources));
+         dispatch<any>(openProjectUpdateDialog(resources[0]));
      },
  };
  
- export const msMoveToAction = {
+ const msMoveToAction: MultiSelectMenuAction = {
+     name: MOVE_TO,
      icon: MoveToIcon,
-     name: "Move to",
+     hasAlts: false,
+     isForMulti: true,
      execute: (dispatch, resource) => {
-         dispatch(openMoveProjectDialog(resource[0]));
+         dispatch<any>(openMoveProjectDialog(resource[0]));
      },
  };
  
- export const msToggleTrashAction = {
-     component: ToggleTrashAction,
-     name: "ToggleTrashAction",
+ const msOpenWith3rdPartyClientAction: MultiSelectMenuAction  = {
+     name: OPEN_W_3RD_PARTY_CLIENT,
+     icon: FolderSharedIcon,
+     hasAlts: false,
+     isForMulti: false,
+     execute: (dispatch, resources) => {
+         dispatch<any>(openWebDavS3InfoDialog(resources[0].uuid));
+     },
+ };
+ export const msToggleTrashAction: MultiSelectMenuAction = {
+     name: MOVE_TO_TRASH,
+     icon: TrashIcon,
+     hasAlts: true,
+     altName: 'Restore from Trash',
+     altIcon: RestoreFromTrashIcon,
+     isForMulti: true,
+     useAlts: (uuid, iconProps) => {
+         return uuid ? (getResource(uuid)(iconProps.resources) as any).isTrashed : false;
+     },
      execute: (dispatch, resources) => {
          for (const resource of [...resources]) {
-             dispatch(toggleProjectTrashed(resource.uuid, resource.ownerUuid, resource.isTrashed!!, resources.length > 1));
+             dispatch<any>(toggleProjectTrashed(resource.uuid, resource.ownerUuid, resource.isTrashed!!, resources.length > 1));
          }
      },
  };
  
- export const msProjectActionSet: ContextMenuActionSet = [[msCopyToClipboardMenuAction, msMoveToAction, msToggleTrashAction]];
+ const msFreezeProjectAction: MultiSelectMenuAction = {
+     name: FREEZE_PROJECT,
+     icon: FreezeIcon,
+     hasAlts: true,
+     altName: 'Unfreeze Project',
+     altIcon: UnfreezeIcon,
+     isForMulti: false,
+     useAlts: (uuid, iconProps) => {
+         return uuid ? !!(getResource(uuid)(iconProps.resources) as any).frozenByUuid : false;
+     },
+     execute: (dispatch, resources) => {
+         if ((resources[0] as any).frozenByUuid) {
+             dispatch<any>(unfreezeProject(resources[0].uuid));
+         } else {
+             dispatch<any>(freezeProject(resources[0].uuid));
+         }
+     },
+ };
+ const msNewProjectAction: MultiSelectMenuAction = {
+     name: NEW_PROJECT,
+     icon: NewProjectIcon,
+     hasAlts: false,
+     isForMulti: false,
+     execute: (dispatch, resources): void => {
+         dispatch<any>(openProjectCreateDialog(resources[0].uuid));
+     },
+ };
+ export const msProjectActionSet: MultiSelectMenuActionSet = [
+     [
+         ...msCommonActionSet,
+         msEditProjectAction,
+         msMoveToAction,
+         msToggleTrashAction,
+         msNewProjectAction,
+         msFreezeProjectAction,
+         msOpenWith3rdPartyClientAction,
+         msCopyToClipboardMenuAction
+     ],
+ ];
+ export const msCommonProjectActionFilter = new Set<string>([
+     ADD_TO_FAVORITES,
+     MOVE_TO_TRASH,
+     API_DETAILS,
+     COPY_TO_CLIPBOARD,
+     EDIT_PROJECT,
+     FREEZE_PROJECT,
+     MOVE_TO,
+     NEW_PROJECT,
+     OPEN_IN_NEW_TAB,
+     OPEN_W_3RD_PARTY_CLIENT,
+     SHARE,
+     VIEW_DETAILS,
+ ]);
+ export const msReadOnlyProjectActionFilter = new Set<string>([ADD_TO_FAVORITES, API_DETAILS, COPY_TO_CLIPBOARD, OPEN_IN_NEW_TAB, OPEN_W_3RD_PARTY_CLIENT, VIEW_DETAILS,]);
+ export const msFrozenProjectActionFilter = new Set<string>([ADD_TO_FAVORITES, API_DETAILS, COPY_TO_CLIPBOARD, OPEN_IN_NEW_TAB, OPEN_W_3RD_PARTY_CLIENT, VIEW_DETAILS, SHARE, FREEZE_PROJECT])
+ export const msAdminFrozenProjectActionFilter = new Set<string>([ADD_TO_FAVORITES, API_DETAILS, COPY_TO_CLIPBOARD, OPEN_IN_NEW_TAB, OPEN_W_3RD_PARTY_CLIENT, VIEW_DETAILS, SHARE, FREEZE_PROJECT, ADD_TO_PUBLIC_FAVORITES])
+ export const msFilterGroupActionFilter = new Set<string>([ADD_TO_FAVORITES, API_DETAILS, COPY_TO_CLIPBOARD, OPEN_IN_NEW_TAB, OPEN_W_3RD_PARTY_CLIENT, VIEW_DETAILS, SHARE, MOVE_TO_TRASH, EDIT_PROJECT, MOVE_TO])
+ export const msAdminFilterGroupActionFilter = new Set<string>([ADD_TO_FAVORITES, API_DETAILS, COPY_TO_CLIPBOARD, OPEN_IN_NEW_TAB, OPEN_W_3RD_PARTY_CLIENT, VIEW_DETAILS, SHARE, MOVE_TO_TRASH, EDIT_PROJECT, MOVE_TO, ADD_TO_PUBLIC_FAVORITES])
index 0000000000000000000000000000000000000000,ab819df22550b3379743bf599a0c640ecfae9115..ab819df22550b3379743bf599a0c640ecfae9115
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,46 +1,46 @@@
+ // Copyright (C) The Arvados Authors. All rights reserved.
+ //
+ // SPDX-License-Identifier: AGPL-3.0
+ import { openRunProcess, deleteWorkflow } from 'store/workflow-panel/workflow-panel-actions';
+ import { StartIcon, TrashIcon, Link } from 'components/icon/icon';
+ import { MultiSelectMenuAction, MultiSelectMenuActionSet, msCommonActionSet } from './ms-menu-actions';
+ import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+ import { copyToClipboardAction } from 'store/open-in-new-tab/open-in-new-tab.actions';
+ const { OPEN_IN_NEW_TAB, COPY_TO_CLIPBOARD, VIEW_DETAILS, API_DETAILS, RUN_WORKFLOW, DELETE_WORKFLOW } = MultiSelectMenuActionNames;
+ const msRunWorkflow: MultiSelectMenuAction = {
+     name: RUN_WORKFLOW,
+     icon: StartIcon,
+     hasAlts: false,
+     isForMulti: false,
+     execute: (dispatch, resources) => {
+         dispatch<any>(openRunProcess(resources[0].uuid, resources[0].ownerUuid, resources[0].name));
+     },
+ };
+ const msDeleteWorkflow: MultiSelectMenuAction = {
+     name: DELETE_WORKFLOW,
+     icon: TrashIcon,
+     hasAlts: false,
+     isForMulti: false,
+     execute: (dispatch, resources) => {
+         dispatch<any>(deleteWorkflow(resources[0].uuid, resources[0].ownerUuid));
+     },
+ };
+ const msCopyToClipboardMenuAction: MultiSelectMenuAction  = {
+     name: COPY_TO_CLIPBOARD,
+     icon: Link,
+     hasAlts: false,
+     isForMulti: false,
+     execute: (dispatch, resources) => {
+         dispatch<any>(copyToClipboardAction(resources));
+     },
+ };
+ export const msWorkflowActionSet: MultiSelectMenuActionSet = [[...msCommonActionSet, msRunWorkflow, msDeleteWorkflow, msCopyToClipboardMenuAction]];
+ export const msReadOnlyWorkflowActionFilter = new Set([OPEN_IN_NEW_TAB, COPY_TO_CLIPBOARD, VIEW_DETAILS, API_DETAILS, RUN_WORKFLOW ]);
+ export const msWorkflowActionFilter = new Set([OPEN_IN_NEW_TAB, COPY_TO_CLIPBOARD, VIEW_DETAILS, API_DETAILS, RUN_WORKFLOW, DELETE_WORKFLOW]);
index ee53f99c3fe94e39c4fa2da096d8e7175d774223,0ccb0502cb28faabe774b4f7b4aba64c2a7b1313..0ccb0502cb28faabe774b4f7b4aba64c2a7b1313
@@@ -31,6 -31,7 +31,7 @@@ import { createTree } from "models/tree
  import { getInitialProcessStatusFilters, getInitialProcessTypeFilters } from "store/resource-type-filters/resource-type-filters";
  import { getProcess } from "store/processes/process";
  import { ResourcesState } from "store/resources/resources";
+ import { toggleOne } from "store/multiselect/multiselect-actions";
  
  type CssRules = "toolbar" | "button" | "root";
  
@@@ -143,6 -144,7 +144,7 @@@ export const AllProcessesPanel = withSt
              };
  
              handleRowClick = (uuid: string) => {
+                 this.props.dispatch<any>(toggleOne(uuid))
                  this.props.dispatch<any>(loadDetailsPanel(uuid));
              };
  
index d93d6e9258673e5dc49979a6262f9a9bef47570d,28983457e6e7c5a5d2821bad5cc782ecd37ee216..28983457e6e7c5a5d2821bad5cc782ecd37ee216
@@@ -350,7 -350,7 +350,7 @@@ export const CollectionDetailsAttribute
          </Grid>
          <Grid item xs={12} md={mdSize}>
              <DetailsAttribute classLabel={classes.label} classValue={classes.value}
-                 label='Storage classes' value={item.storageClassesDesired.join(', ')} />
+                 label='Storage classes' value={item.storageClassesDesired ? item.storageClassesDesired.join(', ') : ["default"]} />
          </Grid>
  
          {/*
index 2392d6fda0380cc2e0108fb8101ae952b18bcf31,aa4f2c1a20a0637d1c3effbf839b6b1219e99974..aa4f2c1a20a0637d1c3effbf839b6b1219e99974
@@@ -38,6 -38,7 +38,7 @@@ import { GroupClass, GroupResource } fr
  import { getProperty } from 'store/properties/properties';
  import { PROJECT_PANEL_CURRENT_UUID } from 'store/project-panel/project-panel-action';
  import { CollectionResource } from 'models/collection';
+ import { toggleOne } from 'store/multiselect/multiselect-actions';
  
  type CssRules = "toolbar" | "button" | "root";
  
@@@ -171,6 -172,7 +172,7 @@@ export const FavoritePanel = withStyles
              }
  
              handleRowClick = (uuid: string) => {
+                 this.props.dispatch<any>(toggleOne(uuid))
                  this.props.dispatch<any>(loadDetailsPanel(uuid));
              }
  
index 4e5c038386f9ce400f0943e9ae80701af29513b4,0000000000000000000000000000000000000000..5c666acd1b6f6e14215e92d2691a52852efde49d
mode 100644,000000..100644
--- /dev/null
@@@ -1,199 -1,0 +1,199 @@@
-     navigateToOutput: (uuid: string) => void;
 +// Copyright (C) The Arvados Authors. All rights reserved.
 +//
 +// SPDX-License-Identifier: AGPL-3.0
 +
 +import React from "react";
 +import { Grid, StyleRulesCallback, withStyles } from "@material-ui/core";
 +import { Dispatch } from 'redux';
 +import { formatCost, formatDate } from "common/formatters";
 +import { resourceLabel } from "common/labels";
 +import { DetailsAttribute } from "components/details-attribute/details-attribute";
 +import { ResourceKind } from "models/resource";
 +import { CollectionName, ContainerRunTime, ResourceWithName } from "views-components/data-explorer/renderers";
 +import { getProcess, getProcessStatus } from "store/processes/process";
 +import { RootState } from "store/store";
 +import { connect } from "react-redux";
 +import { ProcessResource, MOUNT_PATH_CWL_WORKFLOW } from "models/process";
 +import { ContainerResource } from "models/container";
 +import { navigateToOutput, openWorkflow } from "store/process-panel/process-panel-actions";
 +import { ArvadosTheme } from "common/custom-theme";
 +import { ProcessRuntimeStatus } from "views-components/process-runtime-status/process-runtime-status";
 +import { getPropertyChip } from "views-components/resource-properties-form/property-chip";
 +import { ContainerRequestResource } from "models/container-request";
 +import { filterResources } from "store/resources/resources";
 +import { JSONMount } from 'models/mount-types';
 +import { getCollectionUrl } from 'models/collection';
 +
 +type CssRules = 'link' | 'propertyTag';
 +
 +const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 +    link: {
 +        fontSize: '0.875rem',
 +        color: theme.palette.primary.main,
 +        '&:hover': {
 +            cursor: 'pointer'
 +        }
 +    },
 +    propertyTag: {
 +        marginRight: theme.spacing.unit / 2,
 +        marginBottom: theme.spacing.unit / 2
 +    },
 +});
 +
 +const mapStateToProps = (state: RootState, props: { request: ProcessResource }) => {
 +    const process = getProcess(props.request.uuid)(state.resources);
 +
 +    let workflowCollection = "";
 +    let workflowPath = "";
 +    if (process?.containerRequest?.mounts && process.containerRequest.mounts[MOUNT_PATH_CWL_WORKFLOW]) {
 +        const wf = process.containerRequest.mounts[MOUNT_PATH_CWL_WORKFLOW] as JSONMount;
 +
 +        if (wf.content["$graph"] &&
 +            wf.content["$graph"].length > 0 &&
 +            wf.content["$graph"][0] &&
 +            wf.content["$graph"][0]["steps"] &&
 +            wf.content["$graph"][0]["steps"][0]) {
 +
 +            const REGEX = /keep:([0-9a-f]{32}\+\d+)\/(.*)/;
 +            const pdh = wf.content["$graph"][0]["steps"][0].run.match(REGEX);
 +            if (pdh) {
 +                workflowCollection = pdh[1];
 +                workflowPath = pdh[2];
 +            }
 +        }
 +    }
 +
 +    return {
 +        container: process?.container,
 +        workflowCollection,
 +        workflowPath,
 +        subprocesses: filterResources((resource: ContainerRequestResource) =>
 +            resource.kind === ResourceKind.CONTAINER_REQUEST &&
 +            resource.requestingContainerUuid === process?.containerRequest.containerUuid
 +        )(state.resources),
 +    };
 +};
 +
 +interface ProcessDetailsAttributesActionProps {
-     navigateToOutput: (uuid) => dispatch<any>(navigateToOutput(uuid)),
++    navigateToOutput: (resource: ContainerRequestResource) => void;
 +    openWorkflow: (uuid: string) => void;
 +}
 +
 +const mapDispatchToProps = (dispatch: Dispatch): ProcessDetailsAttributesActionProps => ({
-                     {containerRequest.outputUuid && <span onClick={() => props.navigateToOutput(containerRequest.outputUuid!)}>
++    navigateToOutput: (resource) => dispatch<any>(navigateToOutput(resource)),
 +    openWorkflow: (uuid) => dispatch<any>(openWorkflow(uuid)),
 +});
 +
 +export const ProcessDetailsAttributes = withStyles(styles, { withTheme: true })(
 +    connect(mapStateToProps, mapDispatchToProps)(
 +        (props: {
 +            request: ProcessResource, container?: ContainerResource, subprocesses: ContainerRequestResource[],
 +            workflowCollection, workflowPath,
 +            twoCol?: boolean, hideProcessPanelRedundantFields?: boolean, classes: Record<CssRules, string>
 +        } & ProcessDetailsAttributesActionProps) => {
 +            const containerRequest = props.request;
 +            const container = props.container;
 +            const subprocesses = props.subprocesses;
 +            const classes = props.classes;
 +            const mdSize = props.twoCol ? 6 : 12;
 +            const workflowCollection = props.workflowCollection;
 +            const workflowPath = props.workflowPath;
 +            const filteredPropertyKeys = Object.keys(containerRequest.properties)
 +                .filter(k => (typeof containerRequest.properties[k] !== 'object'));
 +            const hasTotalCost = containerRequest && containerRequest.cumulativeCost > 0;
 +            const totalCostNotReady = container && container.cost > 0 && container.state === "Running" && containerRequest && containerRequest.cumulativeCost === 0 && subprocesses.length > 0;
 +            return <Grid container>
 +                <Grid item xs={12}>
 +                    <ProcessRuntimeStatus runtimeStatus={container?.runtimeStatus} containerCount={containerRequest.containerCount} />
 +                </Grid>
 +                {!props.hideProcessPanelRedundantFields && <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROCESS)} />
 +                </Grid>}
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Container request UUID' linkToUuid={containerRequest.uuid} value={containerRequest.uuid} />
 +                </Grid>
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Docker image locator'
 +                        linkToUuid={containerRequest.containerImage} value={containerRequest.containerImage} />
 +                </Grid>
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute
 +                        label='Owner' linkToUuid={containerRequest.ownerUuid}
 +                        uuidEnhancer={(uuid: string) => <ResourceWithName uuid={uuid} />} />
 +                </Grid>
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Container UUID' value={containerRequest.containerUuid} />
 +                </Grid>
 +                {!props.hideProcessPanelRedundantFields && <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Status' value={getProcessStatus({ containerRequest, container })} />
 +                </Grid>}
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Created at' value={formatDate(containerRequest.createdAt)} />
 +                </Grid>
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Started at' value={container ? formatDate(container.startedAt) : "(none)"} />
 +                </Grid>
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Finished at' value={container ? formatDate(container.finishedAt) : "(none)"} />
 +                </Grid>
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Container run time'>
 +                        <ContainerRunTime uuid={containerRequest.uuid} />
 +                    </DetailsAttribute>
 +                </Grid>
 +                {(containerRequest && containerRequest.modifiedByUserUuid) && <Grid item xs={12} md={mdSize} data-cy="process-details-attributes-modifiedby-user">
 +                    <DetailsAttribute
 +                        label='Submitted by' linkToUuid={containerRequest.modifiedByUserUuid}
 +                        uuidEnhancer={(uuid: string) => <ResourceWithName uuid={uuid} />} />
 +                </Grid>}
 +                {(container && container.runtimeUserUuid && container.runtimeUserUuid !== containerRequest.modifiedByUserUuid) && <Grid item xs={12} md={mdSize} data-cy="process-details-attributes-runtime-user">
 +                    <DetailsAttribute
 +                        label='Run as' linkToUuid={container.runtimeUserUuid}
 +                        uuidEnhancer={(uuid: string) => <ResourceWithName uuid={uuid} />} />
 +                </Grid>}
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Requesting container UUID' value={containerRequest.requestingContainerUuid || "(none)"} />
 +                </Grid>
 +                <Grid item xs={6}>
 +                    <DetailsAttribute label='Output collection' />
++                    {containerRequest.outputUuid && <span onClick={() => props.navigateToOutput(containerRequest!)}>
 +                        <CollectionName className={classes.link} uuid={containerRequest.outputUuid} />
 +                    </span>}
 +                </Grid>
 +                {container && <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Cost' value={
 +                        `${hasTotalCost ? formatCost(containerRequest.cumulativeCost) + ' total, ' : (totalCostNotReady ? 'total pending completion, ' : '')}${container.cost > 0 ? formatCost(container.cost) : 'not available'} for this container`
 +                    } />
 +
 +                    {container && workflowCollection && <Grid item xs={12} md={mdSize}>
 +                        <DetailsAttribute label='Workflow code' link={getCollectionUrl(workflowCollection)} value={workflowPath} />
 +                    </Grid>}
 +                </Grid>}
 +                {containerRequest.properties.template_uuid &&
 +                    <Grid item xs={12} md={mdSize}>
 +                        <span onClick={() => props.openWorkflow(containerRequest.properties.template_uuid)}>
 +                            <DetailsAttribute classValue={classes.link}
 +                                label='Workflow' value={containerRequest.properties.workflowName} />
 +                        </span>
 +                    </Grid>}
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Priority' value={containerRequest.priority} />
 +                </Grid>
 +                {/*
 +                      NOTE: The property list should be kept at the bottom, because it spans
 +                      the entire available width, without regards of the twoCol prop.
 +                      */}
 +                <Grid item xs={12} md={12}>
 +                    <DetailsAttribute label='Properties' />
 +                    {filteredPropertyKeys.length > 0
 +                        ? filteredPropertyKeys.map(k =>
 +                            Array.isArray(containerRequest.properties[k])
 +                                ? containerRequest.properties[k].map((v: string) =>
 +                                    getPropertyChip(k, v, undefined, classes.propertyTag))
 +                                : getPropertyChip(k, containerRequest.properties[k], undefined, classes.propertyTag))
 +                        : <div>No properties</div>}
 +                </Grid>
 +            </Grid>;
 +        }
 +    )
 +);
index 2cc751bffd6e78526b38ea07e8d0a5ca4d0683f2,efaf53eb49b21334d87227740cfcaabf9ceaaa33..efaf53eb49b21334d87227740cfcaabf9ceaaa33
@@@ -52,6 -52,7 +52,7 @@@ import { CollectionResource } from 'mod
  import { resourceIsFrozen } from 'common/frozen-resources';
  import { ProjectResource } from 'models/project';
  import { NotFoundView } from 'views/not-found-panel/not-found-panel';
+ import { toggleOne } from 'store/multiselect/multiselect-actions';
  
  type CssRules = 'root' | 'button';
  
@@@ -323,6 -324,7 +324,7 @@@ export const ProjectPanel = withStyles(
              };
  
              handleRowClick = (uuid: string) => {
+                 this.props.dispatch<any>(toggleOne(uuid))
                  this.props.dispatch<any>(loadDetailsPanel(uuid));
              };
          }
index 47c8aedebfc7645ca4cf451c749ce60418bcd1eb,5cb10c4c66b9af0fdf5b71317c4aad9384b2b0af..5cb10c4c66b9af0fdf5b71317c4aad9384b2b0af
@@@ -36,6 -36,7 +36,7 @@@ import { PublicFavoritesState } from 's
  import { getResource, ResourcesState } from 'store/resources/resources';
  import { GroupContentsResource } from 'services/groups-service/groups-service';
  import { CollectionResource } from 'models/collection';
+ import { toggleOne } from 'store/multiselect/multiselect-actions';
  
  type CssRules = "toolbar" | "button" | "root";
  
@@@ -145,7 -146,8 +146,8 @@@ const mapDispatchToProps = (dispatch: D
      },
      onDialogOpen: (ownerUuid: string) => { return; },
      onItemClick: (uuid: string) => {
-         dispatch<any>(loadDetailsPanel(uuid));
+                 dispatch<any>(toggleOne(uuid))
+                 dispatch<any>(loadDetailsPanel(uuid));
      },
      onItemDoubleClick: uuid => {
          dispatch<any>(navigateTo(uuid));
index 0902f15bdcca963a77369b970ff0e2d959cb31f0,320e85cb997afcdf160895e94c17c43b850b2d8f..320e85cb997afcdf160895e94c17c43b850b2d8f
@@@ -13,6 -13,7 +13,7 @@@ import { SearchBarAdvancedFormData } fr
  import { User } from "models/user";
  import { Config } from 'common/config';
  import { Session } from "models/session";
+ import { toggleOne } from "store/multiselect/multiselect-actions";
  
  export interface SearchResultsPanelDataProps {
      data: SearchBarAdvancedFormData;
@@@ -46,6 -47,7 +47,7 @@@ const mapDispatchToProps = (dispatch: D
      },
      onDialogOpen: (ownerUuid: string) => { return; },
      onItemClick: (resourceUuid: string) => {
+         dispatch<any>(toggleOne(resourceUuid))
          dispatch<any>(loadDetailsPanel(resourceUuid));
      },
      onItemDoubleClick: uuid => {
index 250447ea95d10cc927f78ee85aae0df5c5bfe740,f3f827d1469fc27fe8c90d8f543123ba4755698d..f3f827d1469fc27fe8c90d8f543123ba4755698d
@@@ -41,6 -41,7 +41,7 @@@ import 
  } from 'views-components/data-explorer/renderers';
  import { DataTableFilterItem } from 'components/data-table-filters/data-table-filters';
  import { GroupContentsResource } from 'services/groups-service/groups-service';
+ import { toggleOne } from 'store/multiselect/multiselect-actions';
  import { DataColumns } from 'components/data-table/data-table';
  import { ContainerRequestState } from 'models/container-request';
  import { ProjectResource } from 'models/project';
@@@ -280,6 -281,7 +281,7 @@@ export const SharedWithMePanel = withSt
              }
  
              handleRowClick = (uuid: string) => {
+                 this.props.dispatch<any>(toggleOne(uuid))
                  this.props.dispatch<any>(loadDetailsPanel(uuid));
              }
          }
index dd5229bb3568f65af9743dbc155da1f8201f9e96,65c723f6d891864b13b3bd14988b66e56c8a14f6..65c723f6d891864b13b3bd14988b66e56c8a14f6
@@@ -87,7 -87,7 +87,7 @@@ export interface SubprocessPanelDataPro
  }
  
  export interface SubprocessPanelActionProps {
-     onItemClick: (item: string) => void;
+     onRowClick: (item: string) => void;
      onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string, resources: ResourcesState) => void;
      onItemDoubleClick: (item: string) => void;
  }
@@@ -114,7 -114,7 +114,7 @@@ const SubProcessesTitle = withStyles(st
  export const SubprocessPanelRoot = (props: SubprocessPanelProps & MPVPanelProps) => {
      return <DataExplorer
          id={SUBPROCESS_PANEL_ID}
-         onRowClick={props.onItemClick}
+         onRowClick={props.onRowClick}
          onRowDoubleClick={props.onItemDoubleClick}
          onContextMenu={(event, item) => props.onContextMenu(event, item, props.resources)}
          contextMenuColumn={true}
index c52f054b0a0c31e64af3c576420bd43e7b9f27ad,684e1fd2b9c3cd99ef692d104288b4e8d2b1c364..684e1fd2b9c3cd99ef692d104288b4e8d2b1c364
@@@ -10,6 -10,7 +10,7 @@@ import { RootState } from "store/store"
  import { navigateTo } from "store/navigation/navigation-action";
  import { loadDetailsPanel } from "store/details-panel/details-panel-action";
  import { getProcess } from "store/processes/process";
+ import { toggleOne } from 'store/multiselect/multiselect-actions';
  
  const mapDispatchToProps = (dispatch: Dispatch): SubprocessPanelActionProps => ({
      onContextMenu: (event, resourceUuid, resources) => {
@@@ -18,7 -19,8 +19,8 @@@
              dispatch<any>(openProcessContextMenu(event, process));
          }
      },
-     onItemClick: (uuid: string) => {
+     onRowClick: (uuid: string) => {
+         dispatch<any>(toggleOne(uuid))
          dispatch<any>(loadDetailsPanel(uuid));
      },
      onItemDoubleClick: uuid => {
index 350207510555ac30870e21dd19916e9399c27534,2a96ffe0d7cf76f2b34c500dfecde6e6e9f8a071..2a96ffe0d7cf76f2b34c500dfecde6e6e9f8a071
@@@ -35,6 -35,7 +35,7 @@@ import 
      getTrashPanelTypeFilters
  } from 'store/resource-type-filters/resource-type-filters';
  import { CollectionResource } from 'models/collection';
+ import { toggleOne } from 'store/multiselect/multiselect-actions';
  
  type CssRules = "toolbar" | "button" | "root";
  
@@@ -178,6 -179,7 +179,7 @@@ export const TrashPanel = withStyles(st
              }
  
              handleRowClick = (uuid: string) => {
+                 this.props.dispatch<any>(toggleOne(uuid))
                  this.props.dispatch<any>(loadDetailsPanel(uuid));
              }
          }
index 05ea215dd9a2de0716b17a2778eec722ac789c18,0000000000000000000000000000000000000000..b094b769cb0bc93a979b7cf03eee300753f8dcd0
mode 100644,000000..100644
--- /dev/null
@@@ -1,445 -1,0 +1,445 @@@
-     window.addEventListener("resize", () => applyCollapsedState(props.sidePanelIsCollapsed));
 +// Copyright (C) The Arvados Authors. All rights reserved.
 +//
 +// SPDX-License-Identifier: AGPL-3.0
 +
 +import React from "react";
 +import { StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core/styles";
 +import { Route, Switch } from "react-router";
 +import { ProjectPanel } from "views/project-panel/project-panel";
 +import { DetailsPanel } from "views-components/details-panel/details-panel";
 +import { ArvadosTheme } from "common/custom-theme";
 +import { ContextMenu } from "views-components/context-menu/context-menu";
 +import { FavoritePanel } from "../favorite-panel/favorite-panel";
 +import { TokenDialog } from "views-components/token-dialog/token-dialog";
 +import { RichTextEditorDialog } from "views-components/rich-text-editor-dialog/rich-text-editor-dialog";
 +import { Snackbar } from "views-components/snackbar/snackbar";
 +import { CollectionPanel } from "../collection-panel/collection-panel";
 +import { RenameFileDialog } from "views-components/rename-file-dialog/rename-file-dialog";
 +import { FileRemoveDialog } from "views-components/file-remove-dialog/file-remove-dialog";
 +import { MultipleFilesRemoveDialog } from "views-components/file-remove-dialog/multiple-files-remove-dialog";
 +import { Routes } from "routes/routes";
 +import { SidePanel } from "views-components/side-panel/side-panel";
 +import { ProcessPanel } from "views/process-panel/process-panel";
 +import { ChangeWorkflowDialog } from "views-components/run-process-dialog/change-workflow-dialog";
 +import { CreateProjectDialog } from "views-components/dialog-forms/create-project-dialog";
 +import { CreateCollectionDialog } from "views-components/dialog-forms/create-collection-dialog";
 +import { CopyCollectionDialog, CopyMultiCollectionDialog } from "views-components/dialog-forms/copy-collection-dialog";
 +import { CopyProcessDialog } from "views-components/dialog-forms/copy-process-dialog";
 +import { UpdateCollectionDialog } from "views-components/dialog-forms/update-collection-dialog";
 +import { UpdateProcessDialog } from "views-components/dialog-forms/update-process-dialog";
 +import { UpdateProjectDialog } from "views-components/dialog-forms/update-project-dialog";
 +import { MoveProcessDialog } from "views-components/dialog-forms/move-process-dialog";
 +import { MoveProjectDialog } from "views-components/dialog-forms/move-project-dialog";
 +import { MoveCollectionDialog } from "views-components/dialog-forms/move-collection-dialog";
 +import { FilesUploadCollectionDialog } from "views-components/dialog-forms/files-upload-collection-dialog";
 +import { PartialCopyToNewCollectionDialog } from "views-components/dialog-forms/partial-copy-to-new-collection-dialog";
 +import { PartialCopyToExistingCollectionDialog } from "views-components/dialog-forms/partial-copy-to-existing-collection-dialog";
 +import { PartialCopyToSeparateCollectionsDialog } from "views-components/dialog-forms/partial-copy-to-separate-collections-dialog";
 +import { PartialMoveToNewCollectionDialog } from "views-components/dialog-forms/partial-move-to-new-collection-dialog";
 +import { PartialMoveToExistingCollectionDialog } from "views-components/dialog-forms/partial-move-to-existing-collection-dialog";
 +import { PartialMoveToSeparateCollectionsDialog } from "views-components/dialog-forms/partial-move-to-separate-collections-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";
 +import { TrashPanel } from "views/trash-panel/trash-panel";
 +import { SharedWithMePanel } from "views/shared-with-me-panel/shared-with-me-panel";
 +import { RunProcessPanel } from "views/run-process-panel/run-process-panel";
 +import SplitterLayout from "react-splitter-layout";
 +import { WorkflowPanel } from "views/workflow-panel/workflow-panel";
 +import { RegisteredWorkflowPanel } from "views/workflow-panel/registered-workflow-panel";
 +import { SearchResultsPanel } from "views/search-results-panel/search-results-panel";
 +import { SshKeyPanel } from "views/ssh-key-panel/ssh-key-panel";
 +import { SshKeyAdminPanel } from "views/ssh-key-panel/ssh-key-admin-panel";
 +import { SiteManagerPanel } from "views/site-manager-panel/site-manager-panel";
 +import { UserProfilePanel } from "views/user-profile-panel/user-profile-panel";
 +import { SharingDialog } from "views-components/sharing-dialog/sharing-dialog";
 +import { NotFoundDialog } from "views-components/not-found-dialog/not-found-dialog";
 +import { AdvancedTabDialog } from "views-components/advanced-tab-dialog/advanced-tab-dialog";
 +import { ProcessInputDialog } from "views-components/process-input-dialog/process-input-dialog";
 +import { VirtualMachineUserPanel } from "views/virtual-machine-panel/virtual-machine-user-panel";
 +import { VirtualMachineAdminPanel } from "views/virtual-machine-panel/virtual-machine-admin-panel";
 +import { RepositoriesPanel } from "views/repositories-panel/repositories-panel";
 +import { KeepServicePanel } from "views/keep-service-panel/keep-service-panel";
 +import { ApiClientAuthorizationPanel } from "views/api-client-authorization-panel/api-client-authorization-panel";
 +import { LinkPanel } from "views/link-panel/link-panel";
 +import { RepositoriesSampleGitDialog } from "views-components/repositories-sample-git-dialog/repositories-sample-git-dialog";
 +import { RepositoryAttributesDialog } from "views-components/repository-attributes-dialog/repository-attributes-dialog";
 +import { CreateRepositoryDialog } from "views-components/dialog-forms/create-repository-dialog";
 +import { RemoveRepositoryDialog } from "views-components/repository-remove-dialog/repository-remove-dialog";
 +import { CreateSshKeyDialog } from "views-components/dialog-forms/create-ssh-key-dialog";
 +import { PublicKeyDialog } from "views-components/ssh-keys-dialog/public-key-dialog";
 +import { RemoveApiClientAuthorizationDialog } from "views-components/api-client-authorizations-dialog/remove-dialog";
 +import { RemoveKeepServiceDialog } from "views-components/keep-services-dialog/remove-dialog";
 +import { RemoveLinkDialog } from "views-components/links-dialog/remove-dialog";
 +import { RemoveSshKeyDialog } from "views-components/ssh-keys-dialog/remove-dialog";
 +import { VirtualMachineAttributesDialog } from "views-components/virtual-machines-dialog/attributes-dialog";
 +import { RemoveVirtualMachineDialog } from "views-components/virtual-machines-dialog/remove-dialog";
 +import { RemoveVirtualMachineLoginDialog } from "views-components/virtual-machines-dialog/remove-login-dialog";
 +import { VirtualMachineAddLoginDialog } from "views-components/virtual-machines-dialog/add-login-dialog";
 +import { AttributesApiClientAuthorizationDialog } from "views-components/api-client-authorizations-dialog/attributes-dialog";
 +import { AttributesKeepServiceDialog } from "views-components/keep-services-dialog/attributes-dialog";
 +import { AttributesLinkDialog } from "views-components/links-dialog/attributes-dialog";
 +import { AttributesSshKeyDialog } from "views-components/ssh-keys-dialog/attributes-dialog";
 +import { UserPanel } from "views/user-panel/user-panel";
 +import { UserAttributesDialog } from "views-components/user-dialog/attributes-dialog";
 +import { CreateUserDialog } from "views-components/dialog-forms/create-user-dialog";
 +import { HelpApiClientAuthorizationDialog } from "views-components/api-client-authorizations-dialog/help-dialog";
 +import { DeactivateDialog } from "views-components/user-dialog/deactivate-dialog";
 +import { ActivateDialog } from "views-components/user-dialog/activate-dialog";
 +import { SetupDialog } from "views-components/user-dialog/setup-dialog";
 +import { GroupsPanel } from "views/groups-panel/groups-panel";
 +import { RemoveGroupDialog } from "views-components/groups-dialog/remove-dialog";
 +import { GroupAttributesDialog } from "views-components/groups-dialog/attributes-dialog";
 +import { GroupDetailsPanel } from "views/group-details-panel/group-details-panel";
 +import { RemoveGroupMemberDialog } from "views-components/groups-dialog/member-remove-dialog";
 +import { GroupMemberAttributesDialog } from "views-components/groups-dialog/member-attributes-dialog";
 +import { PublicFavoritePanel } from "views/public-favorites-panel/public-favorites-panel";
 +import { LinkAccountPanel } from "views/link-account-panel/link-account-panel";
 +import { FedLogin } from "./fed-login";
 +import { CollectionsContentAddressPanel } from "views/collection-content-address-panel/collection-content-address-panel";
 +import { AllProcessesPanel } from "../all-processes-panel/all-processes-panel";
 +import { NotFoundPanel } from "../not-found-panel/not-found-panel";
 +import { AutoLogout } from "views-components/auto-logout/auto-logout";
 +import { RestoreCollectionVersionDialog } from "views-components/collections-dialog/restore-version-dialog";
 +import { WebDavS3InfoDialog } from "views-components/webdav-s3-dialog/webdav-s3-dialog";
 +import { pluginConfig } from "plugins";
 +import { ElementListReducer } from "common/plugintypes";
 +import { COLLAPSE_ICON_SIZE } from "views-components/side-panel-toggle/side-panel-toggle";
 +import { Banner } from "views-components/baner/banner";
 +import { InstanceTypesPanel } from "views/instance-types-panel/instance-types-panel";
 +
 +type CssRules = "root" | "container" | "splitter" | "asidePanel" | "contentWrapper" | "content";
 +
 +const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 +    root: {
 +        paddingTop: theme.spacing.unit * 7,
 +        background: theme.palette.background.default,
 +    },
 +    container: {
 +        position: "relative",
 +    },
 +    splitter: {
 +        "& > .layout-splitter": {
 +            width: "3px",
 +        },
 +        "& > .layout-splitter-disabled": {
 +            pointerEvents: "none",
 +            cursor: "pointer",
 +        },
 +    },
 +    asidePanel: {
 +        paddingTop: theme.spacing.unit,
 +        height: "100%",
 +    },
 +    contentWrapper: {
 +        paddingTop: theme.spacing.unit,
 +        minWidth: 0,
 +    },
 +    content: {
 +        minWidth: 0,
 +        paddingLeft: theme.spacing.unit * 3,
 +        paddingRight: theme.spacing.unit * 3,
 +        // Reserve vertical space for app bar + MainContentBar
 +        minHeight: `calc(100vh - ${theme.spacing.unit * 16}px)`,
 +        display: "flex",
 +    },
 +});
 +
 +interface WorkbenchDataProps {
 +    isUserActive: boolean;
 +    isNotLinking: boolean;
 +    sessionIdleTimeout: number;
 +    sidePanelIsCollapsed: boolean;
 +}
 +
 +type WorkbenchPanelProps = WithStyles<CssRules> & WorkbenchDataProps;
 +
 +const defaultSplitterSize = 90;
 +
 +const getSplitterInitialSize = () => {
 +    const splitterSize = localStorage.getItem("splitterSize");
 +    return splitterSize ? Number(splitterSize) : defaultSplitterSize;
 +};
 +
 +const saveSplitterSize = (size: number) => localStorage.setItem("splitterSize", size.toString());
 +
 +let routes = (
 +    <>
 +        <Route
 +            path={Routes.PROJECTS}
 +            component={ProjectPanel}
 +        />
 +        <Route
 +            path={Routes.COLLECTIONS}
 +            component={CollectionPanel}
 +        />
 +        <Route
 +            path={Routes.FAVORITES}
 +            component={FavoritePanel}
 +        />
 +        <Route
 +            path={Routes.ALL_PROCESSES}
 +            component={AllProcessesPanel}
 +        />
 +        <Route
 +            path={Routes.PROCESSES}
 +            component={ProcessPanel}
 +        />
 +        <Route
 +            path={Routes.TRASH}
 +            component={TrashPanel}
 +        />
 +        <Route
 +            path={Routes.SHARED_WITH_ME}
 +            component={SharedWithMePanel}
 +        />
 +        <Route
 +            path={Routes.RUN_PROCESS}
 +            component={RunProcessPanel}
 +        />
 +        <Route
 +            path={Routes.REGISTEREDWORKFLOW}
 +            component={RegisteredWorkflowPanel}
 +        />
 +        <Route
 +            path={Routes.WORKFLOWS}
 +            component={WorkflowPanel}
 +        />
 +        <Route
 +            path={Routes.SEARCH_RESULTS}
 +            component={SearchResultsPanel}
 +        />
 +        <Route
 +            path={Routes.VIRTUAL_MACHINES_USER}
 +            component={VirtualMachineUserPanel}
 +        />
 +        <Route
 +            path={Routes.VIRTUAL_MACHINES_ADMIN}
 +            component={VirtualMachineAdminPanel}
 +        />
 +        <Route
 +            path={Routes.REPOSITORIES}
 +            component={RepositoriesPanel}
 +        />
 +        <Route
 +            path={Routes.SSH_KEYS_USER}
 +            component={SshKeyPanel}
 +        />
 +        <Route
 +            path={Routes.SSH_KEYS_ADMIN}
 +            component={SshKeyAdminPanel}
 +        />
 +        <Route
 +            path={Routes.INSTANCE_TYPES}
 +            component={InstanceTypesPanel}
 +        />
 +        <Route
 +            path={Routes.SITE_MANAGER}
 +            component={SiteManagerPanel}
 +        />
 +        <Route
 +            path={Routes.KEEP_SERVICES}
 +            component={KeepServicePanel}
 +        />
 +        <Route
 +            path={Routes.USERS}
 +            component={UserPanel}
 +        />
 +        <Route
 +            path={Routes.API_CLIENT_AUTHORIZATIONS}
 +            component={ApiClientAuthorizationPanel}
 +        />
 +        <Route
 +            path={Routes.MY_ACCOUNT}
 +            component={UserProfilePanel}
 +        />
 +        <Route
 +            path={Routes.USER_PROFILE}
 +            component={UserProfilePanel}
 +        />
 +        <Route
 +            path={Routes.GROUPS}
 +            component={GroupsPanel}
 +        />
 +        <Route
 +            path={Routes.GROUP_DETAILS}
 +            component={GroupDetailsPanel}
 +        />
 +        <Route
 +            path={Routes.LINKS}
 +            component={LinkPanel}
 +        />
 +        <Route
 +            path={Routes.PUBLIC_FAVORITES}
 +            component={PublicFavoritePanel}
 +        />
 +        <Route
 +            path={Routes.LINK_ACCOUNT}
 +            component={LinkAccountPanel}
 +        />
 +        <Route
 +            path={Routes.COLLECTIONS_CONTENT_ADDRESS}
 +            component={CollectionsContentAddressPanel}
 +        />
 +    </>
 +);
 +
 +const reduceRoutesFn: (a: React.ReactElement[], b: ElementListReducer) => React.ReactElement[] = (a, b) => b(a);
 +
 +routes = React.createElement(
 +    React.Fragment,
 +    null,
 +    pluginConfig.centerPanelList.reduce(reduceRoutesFn, React.Children.toArray(routes.props.children))
 +);
 +
 +const applyCollapsedState = isCollapsed => {
 +    const rightPanel: Element = document.getElementsByClassName("layout-pane")[1];
 +    const totalWidth: number = document.getElementsByClassName("splitter-layout")[0]?.clientWidth;
 +    const rightPanelExpandedWidth = (totalWidth - COLLAPSE_ICON_SIZE) / (totalWidth / 100);
 +    if (rightPanel) {
 +        rightPanel.setAttribute("style", `width: ${isCollapsed ? `calc(${rightPanelExpandedWidth}% - 1rem)` : `${getSplitterInitialSize()}%`}`);
 +    }
 +    const splitter = document.getElementsByClassName("layout-splitter")[0];
 +    isCollapsed ? splitter?.classList.add("layout-splitter-disabled") : splitter?.classList.remove("layout-splitter-disabled");
 +};
 +
 +export const WorkbenchPanel = withStyles(styles)((props: WorkbenchPanelProps) => {
 +    //panel size will not scale automatically on window resize, so we do it manually
++    if (props && props.sidePanelIsCollapsed) window.addEventListener("resize", () => applyCollapsedState(props.sidePanelIsCollapsed));
 +    applyCollapsedState(props.sidePanelIsCollapsed);
 +
 +    return (
 +        <Grid
 +            container
 +            item
 +            xs
 +            className={props.classes.root}
 +        >
 +            {props.sessionIdleTimeout > 0 && <AutoLogout />}
 +            <Grid
 +                container
 +                item
 +                xs
 +                className={props.classes.container}
 +            >
 +                <SplitterLayout
 +                    customClassName={props.classes.splitter}
 +                    percentage={true}
 +                    primaryIndex={0}
 +                    primaryMinSize={10}
 +                    secondaryInitialSize={getSplitterInitialSize()}
 +                    secondaryMinSize={40}
 +                    onSecondaryPaneSizeChange={saveSplitterSize}
 +                >
 +                    {props.isUserActive && props.isNotLinking && (
 +                        <Grid
 +                            container
 +                            item
 +                            xs
 +                            component="aside"
 +                            direction="column"
 +                            className={props.classes.asidePanel}
 +                        >
 +                            <SidePanel />
 +                        </Grid>
 +                    )}
 +                    <Grid
 +                        container
 +                        item
 +                        xs
 +                        component="main"
 +                        direction="column"
 +                        className={props.classes.contentWrapper}
 +                    >
 +                        <Grid
 +                            item
 +                            xs
 +                        >
 +                            {props.isNotLinking && <MainContentBar />}
 +                        </Grid>
 +                        <Grid
 +                            item
 +                            xs
 +                            className={props.classes.content}
 +                        >
 +                            <Switch>
 +                                {routes.props.children}
 +                                <Route
 +                                    path={Routes.NO_MATCH}
 +                                    component={NotFoundPanel}
 +                                />
 +                            </Switch>
 +                        </Grid>
 +                    </Grid>
 +                </SplitterLayout>
 +            </Grid>
 +            <Grid item>
 +                <DetailsPanel />
 +            </Grid>
 +            <AdvancedTabDialog />
 +            <AttributesApiClientAuthorizationDialog />
 +            <AttributesKeepServiceDialog />
 +            <AttributesLinkDialog />
 +            <AttributesSshKeyDialog />
 +            <ChangeWorkflowDialog />
 +            <ContextMenu />
 +            <CopyCollectionDialog />
 +            <CopyMultiCollectionDialog />
 +            <CopyProcessDialog />
 +            <CreateCollectionDialog />
 +            <CreateProjectDialog />
 +            <CreateRepositoryDialog />
 +            <CreateSshKeyDialog />
 +            <CreateUserDialog />
 +            <TokenDialog />
 +            <FileRemoveDialog />
 +            <FilesUploadCollectionDialog />
 +            <GroupAttributesDialog />
 +            <GroupMemberAttributesDialog />
 +            <HelpApiClientAuthorizationDialog />
 +            <MoveCollectionDialog />
 +            <MoveProcessDialog />
 +            <MoveProjectDialog />
 +            <MultipleFilesRemoveDialog />
 +            <PublicKeyDialog />
 +            <PartialCopyToNewCollectionDialog />
 +            <PartialCopyToExistingCollectionDialog />
 +            <PartialCopyToSeparateCollectionsDialog />
 +            <PartialMoveToNewCollectionDialog />
 +            <PartialMoveToExistingCollectionDialog />
 +            <PartialMoveToSeparateCollectionsDialog />
 +            <ProcessInputDialog />
 +            <RestoreCollectionVersionDialog />
 +            <RemoveApiClientAuthorizationDialog />
 +            <RemoveGroupDialog />
 +            <RemoveGroupMemberDialog />
 +            <RemoveKeepServiceDialog />
 +            <RemoveLinkDialog />
 +            <RemoveProcessDialog />
 +            <RemoveRepositoryDialog />
 +            <RemoveSshKeyDialog />
 +            <RemoveVirtualMachineDialog />
 +            <RemoveVirtualMachineLoginDialog />
 +            <VirtualMachineAddLoginDialog />
 +            <RenameFileDialog />
 +            <RepositoryAttributesDialog />
 +            <RepositoriesSampleGitDialog />
 +            <RichTextEditorDialog />
 +            <SharingDialog />
 +            <NotFoundDialog />
 +            <Snackbar />
 +            <UpdateCollectionDialog />
 +            <UpdateProcessDialog />
 +            <UpdateProjectDialog />
 +            <UserAttributesDialog />
 +            <DeactivateDialog />
 +            <ActivateDialog />
 +            <SetupDialog />
 +            <VirtualMachineAttributesDialog />
 +            <FedLogin />
 +            <WebDavS3InfoDialog />
 +            <Banner />
 +            {React.createElement(React.Fragment, null, pluginConfig.dialogs)}
 +        </Grid>
 +    );
 +});