From: Pawel Kromplewski Date: Mon, 3 Dec 2018 07:29:41 +0000 (+0100) Subject: Merge branch 'master' into 14452-my-account X-Git-Tag: 1.4.0~92^2~6 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/4ea2ff188ec745966387ce8bbe14880bfeede863?hp=92dd5d32b573b5c90f3ce72dc207f6d0e7f21178 Merge branch 'master' into 14452-my-account refs #14452 Arvados-DCO-1.1-Signed-off-by: Pawel Kromplewski --- diff --git a/README.md b/README.md index 998d4246..ea9bc02f 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,8 @@ The app will fetch runtime configuration when starting. By default it will try t Currently this configuration schema is supported: ``` { - "API_HOST": "string" + "API_HOST": "string", + "VOCABULARY_URL": "string" } ``` diff --git a/package.json b/package.json index 89458046..64a24dca 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,9 @@ "scripts": { "start": "react-scripts-ts start", "build": "REACT_APP_BUILD_NUMBER=$BUILD_NUMBER REACT_APP_GIT_COMMIT=$GIT_COMMIT react-scripts-ts build", + "build-local": "react-scripts-ts build", "test": "CI=true react-scripts-ts test --env=jsdom", + "test-local": "react-scripts-ts test --env=jsdom", "eject": "react-scripts-ts eject", "lint": "tslint src/** -t verbose", "build-css": "node-sass-chokidar src/ -o src/", diff --git a/src/common/config.ts b/src/common/config.ts index 1ab73294..c74277e4 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -49,6 +49,7 @@ export interface Config { version: string; websocketUrl: string; workbenchUrl: string; + vocabularyUrl: string; } export const fetchConfig = () => { @@ -58,7 +59,10 @@ export const fetchConfig = () => { .catch(() => Promise.resolve(getDefaultConfig())) .then(config => Axios .get(getDiscoveryURL(config.API_HOST)) - .then(response => ({ config: response.data, apiHost: config.API_HOST }))); + .then(response => ({ + config: {...response.data, vocabularyUrl: config.VOCABULARY_URL }, + apiHost: config.API_HOST, + }))); }; @@ -105,15 +109,18 @@ export const mockConfig = (config: Partial): Config => ({ version: '', websocketUrl: '', workbenchUrl: '', + vocabularyUrl: '', ...config }); interface ConfigJSON { API_HOST: string; + VOCABULARY_URL: string; } const getDefaultConfig = (): ConfigJSON => ({ API_HOST: process.env.REACT_APP_ARVADOS_API_HOST || "", + VOCABULARY_URL: "", }); const getDiscoveryURL = (apiHost: string) => `${window.location.protocol}//${apiHost}/discovery/v1/apis/arvados/v1/rest`; diff --git a/src/components/autocomplete/autocomplete.tsx b/src/components/autocomplete/autocomplete.tsx index 85704c35..7da4ba4a 100644 --- a/src/components/autocomplete/autocomplete.tsx +++ b/src/components/autocomplete/autocomplete.tsx @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; -import { Input as MuiInput, Chip as MuiChip, Popper as MuiPopper, Paper, FormControl, InputLabel, StyleRulesCallback, withStyles, RootRef, ListItemText, ListItem, List } from '@material-ui/core'; +import { Input as MuiInput, Chip as MuiChip, Popper as MuiPopper, Paper, FormControl, InputLabel, StyleRulesCallback, withStyles, RootRef, ListItemText, ListItem, List, FormHelperText } from '@material-ui/core'; import { PopperProps } from '@material-ui/core/Popper'; import { WithStyles } from '@material-ui/core/styles'; import { noop } from 'lodash'; @@ -13,6 +13,8 @@ export interface AutocompleteProps { value: string; items: Item[]; suggestions?: Suggestion[]; + error?: boolean; + helperText?: string; onChange: (event: React.ChangeEvent) => void; onBlur?: (event: React.FocusEvent) => void; onFocus?: (event: React.FocusEvent) => void; @@ -38,9 +40,10 @@ export class Autocomplete extends React.Component - + {this.renderLabel()} {this.renderInput()} + {this.renderHelperText()} {this.renderSuggestions()} @@ -64,12 +67,16 @@ export class Autocomplete extends React.Component; } + renderHelperText(){ + return {this.props.helperText}; + } + renderSuggestions() { const { suggestions = [] } = this.props; return ( 0} - anchorEl={this.containerRef.current}> + anchorEl={this.inputRef.current}> {suggestions.map( diff --git a/src/components/data-explorer/data-explorer.tsx b/src/components/data-explorer/data-explorer.tsx index f863ba13..cb979c7b 100644 --- a/src/components/data-explorer/data-explorer.tsx +++ b/src/components/data-explorer/data-explorer.tsx @@ -7,9 +7,10 @@ import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, WithStyles, Table import { ColumnSelector } from "../column-selector/column-selector"; import { DataTable, DataColumns } from "../data-table/data-table"; import { DataColumn, SortDirection } from "../data-table/data-column"; -import { DataTableFilterItem } from '../data-table-filters/data-table-filters'; import { SearchInput } from '../search-input/search-input'; import { ArvadosTheme } from "~/common/custom-theme"; +import { createTree } from '~/models/tree'; +import { DataTableFilters } from '../data-table-filters/data-table-filters-tree'; import { MoreOptionsIcon } from '~/components/icon/icon'; type CssRules = 'searchBox' | "toolbar" | "footer" | "root" | 'moreOptionsButton'; @@ -53,7 +54,7 @@ interface DataExplorerActionProps { onColumnToggle: (column: DataColumn) => void; onContextMenu: (event: React.MouseEvent, item: T) => void; onSortToggle: (column: DataColumn) => void; - onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn) => void; + onFiltersChange: (filters: DataTableFilters, column: DataColumn) => void; onChangePage: (page: number) => void; onChangeRowsPerPage: (rowsPerPage: number) => void; extractKey?: (item: T) => React.Key; @@ -137,7 +138,7 @@ export const DataExplorer = withStyles(styles)( selected: true, configurable: false, sortDirection: SortDirection.NONE, - filters: [], + filters: createTree(), key: "context-actions", render: this.renderContextMenuTrigger }; diff --git a/src/components/data-table-filters/data-table-filters-popover.tsx b/src/components/data-table-filters/data-table-filters-popover.tsx new file mode 100644 index 00000000..b79d36b4 --- /dev/null +++ b/src/components/data-table-filters/data-table-filters-popover.tsx @@ -0,0 +1,165 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from "react"; +import { + WithStyles, + withStyles, + ButtonBase, + StyleRulesCallback, + Theme, + Popover, + Button, + Card, + CardActions, + Typography, + CardContent, + Tooltip +} from "@material-ui/core"; +import * as classnames from "classnames"; +import { DefaultTransformOrigin } from "~/components/popover/helpers"; +import { createTree } from '~/models/tree'; +import { DataTableFilters, DataTableFiltersTree } from "./data-table-filters-tree"; +import { getNodeDescendants } from '~/models/tree'; + +export type CssRules = "root" | "icon" | "active" | "checkbox"; + +const styles: StyleRulesCallback = (theme: Theme) => ({ + root: { + cursor: "pointer", + display: "inline-flex", + justifyContent: "flex-start", + flexDirection: "inherit", + alignItems: "center", + "&:hover": { + color: theme.palette.text.primary, + }, + "&:focus": { + color: theme.palette.text.primary, + }, + }, + active: { + color: theme.palette.text.primary, + '& $icon': { + opacity: 1, + }, + }, + icon: { + marginRight: 4, + marginLeft: 4, + opacity: 0.7, + userSelect: "none", + width: 16 + }, + checkbox: { + width: 24, + height: 24 + } +}); + +export interface DataTableFilterProps { + name: string; + filters: DataTableFilters; + onChange?: (filters: DataTableFilters) => void; +} + +interface DataTableFilterState { + anchorEl?: HTMLElement; + filters: DataTableFilters; + prevFilters: DataTableFilters; +} + +export const DataTableFiltersPopover = withStyles(styles)( + class extends React.Component, DataTableFilterState> { + state: DataTableFilterState = { + anchorEl: undefined, + filters: createTree(), + prevFilters: createTree(), + }; + icon = React.createRef(); + + render() { + const { name, classes, children } = this.props; + const isActive = getNodeDescendants('')(this.state.filters).some(f => f.selected); + return <> + + + {children} + + + + + + + + {name} + + + this.setState({ filters })} /> + + + + + + + ; + } + + static getDerivedStateFromProps(props: DataTableFilterProps, state: DataTableFilterState): DataTableFilterState { + return props.filters !== state.prevFilters + ? { ...state, filters: props.filters, prevFilters: props.filters } + : state; + } + + open = () => { + this.setState({ anchorEl: this.icon.current || undefined }); + } + + submit = () => { + const { onChange } = this.props; + if (onChange) { + onChange(this.state.filters); + } + this.setState({ anchorEl: undefined }); + } + + cancel = () => { + this.setState(prev => ({ + ...prev, + filters: prev.prevFilters, + anchorEl: undefined + })); + } + + setFilters = (filters: DataTableFilters) => { + this.setState({ filters }); + } + + } +); diff --git a/src/components/data-table-filters/data-table-filters-tree.tsx b/src/components/data-table-filters/data-table-filters-tree.tsx new file mode 100644 index 00000000..dcc4f0e1 --- /dev/null +++ b/src/components/data-table-filters/data-table-filters-tree.tsx @@ -0,0 +1,74 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from "react"; +import { Tree, toggleNodeSelection, getNode, initTreeNode, getNodeChildrenIds } from '~/models/tree'; +import { Tree as TreeComponent, TreeItem, TreeItemStatus } from '~/components/tree/tree'; +import { noop, map } from "lodash/fp"; +import { toggleNodeCollapse } from '~/models/tree'; +import { countNodes, countChildren } from '~/models/tree'; + +export interface DataTableFilterItem { + name: string; +} + +export type DataTableFilters = Tree; + +export interface DataTableFilterProps { + filters: DataTableFilters; + onChange?: (filters: DataTableFilters) => void; +} + +export class DataTableFiltersTree extends React.Component { + + render() { + const { filters } = this.props; + const hasSubfilters = countNodes(filters) !== countChildren('')(filters); + return ; + } + + toggleFilter = (_: React.MouseEvent, item: TreeItem) => { + const { onChange = noop } = this.props; + onChange(toggleNodeSelection(item.id)(this.props.filters)); + } + + toggleOpen = (_: React.MouseEvent, item: TreeItem) => { + const { onChange = noop } = this.props; + onChange(toggleNodeCollapse(item.id)(this.props.filters)); + } +} + +const renderItem = (item: TreeItem) => + {item.data.name}; + +const filterToTreeItem = (filters: DataTableFilters) => + (id: string): TreeItem => { + const node = getNode(id)(filters) || initTreeNode({ id: '', value: 'InvalidNode' }); + const items = getNodeChildrenIds(node.id)(filters) + .map(filterToTreeItem(filters)); + + return { + active: node.active, + data: node.value, + id: node.id, + items: items.length > 0 ? items : undefined, + open: node.expanded, + selected: node.selected, + status: TreeItemStatus.LOADED, + }; + }; + +const filtersToTree = (filters: DataTableFilters): TreeItem[] => + map(filterToTreeItem(filters), getNodeChildrenIds('')(filters)); diff --git a/src/components/data-table-filters/data-table-filters.tsx b/src/components/data-table-filters/data-table-filters.tsx index 11607e4b..7033d369 100644 --- a/src/components/data-table-filters/data-table-filters.tsx +++ b/src/components/data-table-filters/data-table-filters.tsx @@ -23,6 +23,10 @@ import { } from "@material-ui/core"; import * as classnames from "classnames"; import { DefaultTransformOrigin } from "../popover/helpers"; +import { createTree, initTreeNode, mapTree } from '~/models/tree'; +import { DataTableFilters as DataTableFiltersModel, DataTableFiltersTree } from "./data-table-filters-tree"; +import { pipe } from 'lodash/fp'; +import { setNode } from '~/models/tree'; export type CssRules = "root" | "icon" | "active" | "checkbox"; @@ -74,14 +78,27 @@ interface DataTableFilterState { anchorEl?: HTMLElement; filters: DataTableFilterItem[]; prevFilters: DataTableFilterItem[]; + filtersTree: DataTableFiltersModel; } +const filters: DataTableFiltersModel = pipe( + createTree, + setNode(initTreeNode({ id: 'Project', value: { name: 'Project' } })), + setNode(initTreeNode({ id: 'Process', value: { name: 'Process' } })), + setNode(initTreeNode({ id: 'Data collection', value: { name: 'Data collection' } })), + setNode(initTreeNode({ id: 'General', parent: 'Data collection', value: { name: 'General' } })), + setNode(initTreeNode({ id: 'Output', parent: 'Data collection', value: { name: 'Output' } })), + setNode(initTreeNode({ id: 'Log', parent: 'Data collection', value: { name: 'Log' } })), + mapTree(node => ({...node, selected: true})), +)(); + export const DataTableFilters = withStyles(styles)( class extends React.Component, DataTableFilterState> { state: DataTableFilterState = { anchorEl: undefined, filters: [], - prevFilters: [] + prevFilters: [], + filtersTree: filters, }; icon = React.createRef(); @@ -128,6 +145,9 @@ export const DataTableFilters = withStyles(styles)( )} + this.setState({ filtersTree })} /> ; + +const Progress = withStyles({ + root: { + position: 'absolute', + }, +})(CircularProgress); + +const getProgressSize = (size?: 'small' | 'medium' | 'large') => { + switch (size) { + case 'small': + return 16; + case 'large': + return 24; + default: + return 20; + } +}; diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx index 4cbefbd2..c64a7221 100644 --- a/src/components/tree/tree.tsx +++ b/src/components/tree/tree.tsx @@ -88,6 +88,8 @@ export interface TreeProps { onContextMenu: (event: React.MouseEvent, item: TreeItem) => void; render: (item: TreeItem, level?: number) => ReactElement<{}>; showSelection?: boolean | ((item: TreeItem) => boolean); + levelIndentation?: number; + itemRightPadding?: number; toggleItemActive: (event: React.MouseEvent, item: TreeItem) => void; toggleItemOpen: (event: React.MouseEvent, item: TreeItem) => void; toggleItemSelection?: (event: React.MouseEvent, item: TreeItem) => void; @@ -103,10 +105,16 @@ export const Tree = withStyles(styles)( ? this.props.showSelection : () => this.props.showSelection ? true : false; + const { levelIndentation = 20, itemRightPadding = 20 } = this.props; + return {items && items.map((it: TreeItem, idx: number) =>
- toggleItemActive(event, it)} onContextMenu={this.handleRowContextMenu(it)}> diff --git a/src/index.tsx b/src/index.tsx index 88fd2298..801a56a1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -50,6 +50,7 @@ import HTML5Backend from 'react-dnd-html5-backend'; import { initAdvanceFormProjectsTree } from '~/store/search-bar/search-bar-actions'; import { repositoryActionSet } from '~/views-components/context-menu/action-sets/repository-action-set'; import { sshKeyActionSet } from '~/views-components/context-menu/action-sets/ssh-key-action-set'; +import { loadVocabulary } from '~/store/vocabulary/vocabulary-actions'; console.log(`Starting arvados [${getBuildInfo()}]`); @@ -88,6 +89,7 @@ fetchConfig() store.dispatch(setBuildInfo()); store.dispatch(setCurrentTokenDialogApiHost(apiHost)); store.dispatch(setUuidPrefix(config.uuidPrefix)); + store.dispatch(loadVocabulary); const TokenComponent = (props: any) => ; const MainPanelComponent = (props: any) => ; @@ -304,3 +306,4 @@ const createSampleProcess = ({ containerRequestService }: ServiceRepository) => }); }; +// force build comment #1 diff --git a/src/models/collection.ts b/src/models/collection.ts index 53c62301..2b16ea25 100644 --- a/src/models/collection.ts +++ b/src/models/collection.ts @@ -22,3 +22,9 @@ export interface CollectionResource extends TrashableResource { export const getCollectionUrl = (uuid: string) => { return `/collections/${uuid}`; }; + +export enum CollectionType { + GENERAL = 'nil', + OUTPUT = 'output', + LOG = 'log', +} diff --git a/src/models/link.ts b/src/models/link.ts index 9d1711d8..baaff658 100644 --- a/src/models/link.ts +++ b/src/models/link.ts @@ -3,13 +3,14 @@ // SPDX-License-Identifier: AGPL-3.0 import { Resource } from "./resource"; +import { TagProperty } from "~/models/tag"; export interface LinkResource extends Resource { headUuid: string; tailUuid: string; linkClass: string; name: string; - properties: {}; + properties: TagProperty; } export enum LinkClass { diff --git a/src/models/resource.ts b/src/models/resource.ts index 5fa61797..7e2127b2 100644 --- a/src/models/resource.ts +++ b/src/models/resource.ts @@ -31,6 +31,7 @@ export enum ResourceKind { REPOSITORY = "arvados#repository", SSH_KEY = "arvados#authorizedKeys", USER = "arvados#user", + VIRTUAL_MACHINE = "arvados#virtualMachine", WORKFLOW = "arvados#workflow", NONE = "arvados#none" } @@ -43,7 +44,9 @@ export enum ResourceObjectType { LOG = '57u5n', REPOSITORY = 's0uqq', USER = 'tpzed', + VIRTUAL_MACHINE = '2x53u', WORKFLOW = '7fd4e', + SSH_KEY = 'fngyi' } export const RESOURCE_UUID_PATTERN = '.{5}-.{5}-.{15}'; @@ -76,8 +79,12 @@ export const extractUuidKind = (uuid: string = '') => { return ResourceKind.LOG; case ResourceObjectType.WORKFLOW: return ResourceKind.WORKFLOW; + case ResourceObjectType.VIRTUAL_MACHINE: + return ResourceKind.VIRTUAL_MACHINE; case ResourceObjectType.REPOSITORY: return ResourceKind.REPOSITORY; + case ResourceObjectType.SSH_KEY: + return ResourceKind.SSH_KEY; default: return undefined; } diff --git a/src/models/tree.ts b/src/models/tree.ts index fe52a97b..bec2f758 100644 --- a/src/models/tree.ts +++ b/src/models/tree.ts @@ -95,6 +95,12 @@ export const getNodeAncestorsIds = (id: string) => (tree: Tree): string[] export const getNodeDescendants = (id: string, limit = Infinity) => (tree: Tree) => mapIdsToNodes(getNodeDescendantsIds(id, limit)(tree))(tree); +export const countNodes = (tree: Tree) => + getNodeDescendantsIds('')(tree).length; + +export const countChildren = (id: string) => (tree: Tree) => + getNodeChildren('')(tree).length; + export const getNodeDescendantsIds = (id: string, limit = Infinity) => (tree: Tree): string[] => { const node = getNode(id)(tree); const children = node ? node.children : @@ -120,19 +126,19 @@ export const mapIdsToNodes = (ids: string[]) => (tree: Tree) => ids.map(id => getNode(id)(tree)).filter((node): node is TreeNode => node !== undefined); export const activateNode = (id: string) => (tree: Tree) => - mapTree(node => node.id === id ? { ...node, active: true } : { ...node, active: false })(tree); + mapTree((node: TreeNode) => node.id === id ? { ...node, active: true } : { ...node, active: false })(tree); export const deactivateNode = (tree: Tree) => - mapTree(node => node.active ? { ...node, active: false } : node)(tree); + mapTree((node: TreeNode) => node.active ? { ...node, active: false } : node)(tree); export const expandNode = (...ids: string[]) => (tree: Tree) => - mapTree(node => ids.some(id => id === node.id) ? { ...node, expanded: true } : node)(tree); + mapTree((node: TreeNode) => ids.some(id => id === node.id) ? { ...node, expanded: true } : node)(tree); export const collapseNode = (...ids: string[]) => (tree: Tree) => - mapTree(node => ids.some(id => id === node.id) ? { ...node, expanded: false } : node)(tree); + mapTree((node: TreeNode) => ids.some(id => id === node.id) ? { ...node, expanded: false } : node)(tree); export const toggleNodeCollapse = (...ids: string[]) => (tree: Tree) => - mapTree(node => ids.some(id => id === node.id) ? { ...node, expanded: !node.expanded } : node)(tree); + mapTree((node: TreeNode) => ids.some(id => id === node.id) ? { ...node, expanded: !node.expanded } : node)(tree); export const setNodeStatus = (id: string) => (status: TreeNodeStatus) => (tree: Tree) => { const node = getNode(id)(tree); @@ -175,6 +181,10 @@ export const deselectNodes = (id: string | string[]) => (tree: Tree) => { return ids.reduce((tree, id) => deselectNode(id)(tree), tree); }; +export const getSelectedNodes = (tree: Tree) => + getNodeDescendants('')(tree) + .filter(node => node.selected); + export const initTreeNode = (data: Pick, 'id' | 'value'> & { parent?: string }): TreeNode => ({ children: [], active: false, diff --git a/src/models/user.ts b/src/models/user.ts index eed135a2..dfb41889 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -22,6 +22,7 @@ export interface User { ownerUuid: string; identityUrl: string; prefs: userPrefs; + isAdmin: boolean; } export const getUserFullname = (user?: User) => { diff --git a/src/models/virtual-machines.ts b/src/models/virtual-machines.ts new file mode 100644 index 00000000..0652c350 --- /dev/null +++ b/src/models/virtual-machines.ts @@ -0,0 +1,18 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { Resource } from "~/models/resource"; + +export interface VirtualMachinesResource extends Resource { + hostname: string; +} + +export interface VirtualMachinesLoginsResource { + hostname: string; + username: string; + public_key: string; + user_uuid: string; + virtual_machine_uuid: string; + authorized_key_uuid: string; +} \ No newline at end of file diff --git a/src/models/vocabulary.ts b/src/models/vocabulary.ts new file mode 100644 index 00000000..ea23ad2c --- /dev/null +++ b/src/models/vocabulary.ts @@ -0,0 +1,24 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { isObject, has, every } from 'lodash/fp'; + +export interface Vocabulary { + strict: boolean; + tags: Record; +} + +export interface Tag { + strict?: boolean; + values?: string[]; +} + +const VOCABULARY_VALIDATORS = [ + isObject, + has('strict'), + has('tags'), +]; + +export const isVocabulary = (value: any) => + every(validator => validator(value), VOCABULARY_VALIDATORS); \ No newline at end of file diff --git a/src/routes/route-change-handlers.ts b/src/routes/route-change-handlers.ts index f2aed2db..1f3bd93f 100644 --- a/src/routes/route-change-handlers.ts +++ b/src/routes/route-change-handlers.ts @@ -6,6 +6,8 @@ import { History, Location } from 'history'; import { RootStore } from '~/store/store'; import { matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute, matchTrashRoute, matchRootRoute, matchSharedWithMeRoute, matchRunProcessRoute, matchWorkflowRoute, matchSearchResultsRoute, matchSshKeysRoute, matchRepositoriesRoute, matchMyAccountRoute } from './routes'; import { loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog, loadSshKeys, loadRepositories, loadMyAccount } from '~/store/workbench/workbench-actions'; +import { matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute, matchTrashRoute, matchRootRoute, matchSharedWithMeRoute, matchRunProcessRoute, matchWorkflowRoute, matchSearchResultsRoute, matchSshKeysRoute, matchRepositoriesRoute, matchVirtualMachineRoute } from './routes'; +import { loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog, loadSshKeys, loadRepositories, loadVirtualMachines } from '~/store/workbench/workbench-actions'; import { navigateToRootProject } from '~/store/navigation/navigation-action'; import { loadSharedWithMe, loadRunProcess, loadWorkflow, loadSearchResults } from '~//store/workbench/workbench-actions'; @@ -27,6 +29,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => { const searchResultsMatch = matchSearchResultsRoute(pathname); const sharedWithMeMatch = matchSharedWithMeRoute(pathname); const runProcessMatch = matchRunProcessRoute(pathname); + const virtualMachineMatch = matchVirtualMachineRoute(pathname); const workflowMatch = matchWorkflowRoute(pathname); const sshKeysMatch = matchSshKeysRoute(pathname); const myAccountMatch = matchMyAccountRoute(pathname); @@ -53,6 +56,8 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => { store.dispatch(loadWorkflow); } else if (searchResultsMatch) { store.dispatch(loadSearchResults); + } else if (virtualMachineMatch) { + store.dispatch(loadVirtualMachines); } else if(repositoryMatch) { store.dispatch(loadRepositories); } else if (sshKeysMatch) { diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 988b4cb4..a27b4274 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -19,6 +19,7 @@ export const Routes = { REPOSITORIES: '/repositories', SHARED_WITH_ME: '/shared-with-me', RUN_PROCESS: '/run-process', + VIRTUAL_MACHINES: '/virtual-machines', WORKFLOWS: '/workflows', SEARCH_RESULTS: '/search-results', SSH_KEYS: `/ssh-keys`, @@ -80,6 +81,9 @@ export const matchWorkflowRoute = (route: string) => export const matchSearchResultsRoute = (route: string) => matchPath(route, { path: Routes.SEARCH_RESULTS }); +export const matchVirtualMachineRoute = (route: string) => + matchPath(route, { path: Routes.VIRTUAL_MACHINES }); + export const matchRepositoriesRoute = (route: string) => matchPath(route, { path: Routes.REPOSITORIES }); diff --git a/src/services/api/filter-builder.test.ts b/src/services/api/filter-builder.test.ts index 5f646de5..2ddd6bf0 100644 --- a/src/services/api/filter-builder.test.ts +++ b/src/services/api/filter-builder.test.ts @@ -60,6 +60,12 @@ describe("FilterBuilder", () => { ).toEqual(`["etag","in",["etagValue1","etagValue2"]]`); }); + it("should add 'not in' rule for set", () => { + expect( + filters.addNotIn("etag", ["etagValue1", "etagValue2"]).getFilters() + ).toEqual(`["etag","not in",["etagValue1","etagValue2"]]`); + }); + it("should add multiple rules", () => { expect( filters @@ -73,6 +79,6 @@ describe("FilterBuilder", () => { expect(new FilterBuilder() .addIn("etag", ["etagValue1", "etagValue2"], "myPrefix") .getFilters()) - .toEqual(`["my_prefix.etag","in",["etagValue1","etagValue2"]]`); + .toEqual(`["myPrefix.etag","in",["etagValue1","etagValue2"]]`); }); }); diff --git a/src/services/api/filter-builder.ts b/src/services/api/filter-builder.ts index e36765ba..1ebf4886 100644 --- a/src/services/api/filter-builder.ts +++ b/src/services/api/filter-builder.ts @@ -31,6 +31,10 @@ export class FilterBuilder { return this.addCondition(field, "in", value, "", "", resourcePrefix); } + public addNotIn(field: string, value?: string | string[], resourcePrefix?: string) { + return this.addCondition(field, "not in", value, "", "", resourcePrefix); + } + public addGt(field: string, value?: string, resourcePrefix?: string) { return this.addCondition(field, ">", value, "", "", resourcePrefix); } @@ -62,7 +66,7 @@ export class FilterBuilder { } const resPrefix = resourcePrefix - ? _.snakeCase(resourcePrefix) + "." + ? resourcePrefix + "." : ""; this.filters += `${this.filters ? "," : ""}["${resPrefix}${_.snakeCase(field)}","${cond}",${value}]`; diff --git a/src/services/auth-service/auth-service.ts b/src/services/auth-service/auth-service.ts index d4e81e42..6faaf99e 100644 --- a/src/services/auth-service/auth-service.ts +++ b/src/services/auth-service/auth-service.ts @@ -13,6 +13,7 @@ export const USER_FIRST_NAME_KEY = 'userFirstName'; export const USER_LAST_NAME_KEY = 'userLastName'; export const USER_UUID_KEY = 'userUuid'; export const USER_OWNER_UUID_KEY = 'userOwnerUuid'; +export const USER_IS_ADMIN = 'isAdmin'; export const USER_IDENTITY_URL = 'identityUrl'; export const USER_PREFS = 'prefs'; @@ -54,16 +55,22 @@ export class AuthService { return localStorage.getItem(USER_OWNER_UUID_KEY) || undefined; } + public getIsAdmin(): boolean { + return !!localStorage.getItem(USER_IS_ADMIN); + } + public getUser(): User | undefined { const email = localStorage.getItem(USER_EMAIL_KEY); const firstName = localStorage.getItem(USER_FIRST_NAME_KEY); const lastName = localStorage.getItem(USER_LAST_NAME_KEY); - const uuid = localStorage.getItem(USER_UUID_KEY); - const ownerUuid = localStorage.getItem(USER_OWNER_UUID_KEY); + const uuid = this.getUuid(); + const ownerUuid = this.getOwnerUuid(); + const isAdmin = this.getIsAdmin(); const identityUrl = localStorage.getItem(USER_IDENTITY_URL); const prefs = JSON.parse(localStorage.getItem(USER_PREFS) || '{"profile": {}}'); + return email && firstName && lastName && uuid && ownerUuid && identityUrl && prefs - ? { email, firstName, lastName, uuid, ownerUuid, identityUrl, prefs } + ? { email, firstName, lastName, uuid, ownerUuid, isAdmin, identityUrl, prefs } : undefined; } @@ -73,6 +80,7 @@ export class AuthService { localStorage.setItem(USER_LAST_NAME_KEY, user.lastName); localStorage.setItem(USER_UUID_KEY, user.uuid); localStorage.setItem(USER_OWNER_UUID_KEY, user.ownerUuid); + localStorage.setItem(USER_IS_ADMIN, JSON.stringify(user.isAdmin)); localStorage.setItem(USER_IDENTITY_URL, user.identityUrl); localStorage.setItem(USER_PREFS, JSON.stringify(user.prefs)); } @@ -83,6 +91,7 @@ export class AuthService { localStorage.removeItem(USER_LAST_NAME_KEY); localStorage.removeItem(USER_UUID_KEY); localStorage.removeItem(USER_OWNER_UUID_KEY); + localStorage.removeItem(USER_IS_ADMIN); localStorage.removeItem(USER_IDENTITY_URL); localStorage.removeItem(USER_PREFS); } @@ -112,6 +121,7 @@ export class AuthService { lastName: resp.data.last_name, uuid: resp.data.uuid, ownerUuid: resp.data.owner_uuid, + isAdmin: resp.data.is_admin, identityUrl: resp.data.identity_url, prefs }; diff --git a/src/services/services.ts b/src/services/services.ts index 308505c5..b24b1d99 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -24,8 +24,10 @@ import { ApiActions } from "~/services/api/api-actions"; import { WorkflowService } from "~/services/workflow-service/workflow-service"; import { SearchService } from '~/services/search-service/search-service'; import { PermissionService } from "~/services/permission-service/permission-service"; +import { VirtualMachinesService } from "~/services/virtual-machines-service/virtual-machines-service"; import { RepositoriesService } from '~/services/repositories-service/repositories-service'; import { AuthorizedKeysService } from '~/services/authorized-keys-service/authorized-keys-service'; +import { VocabularyService } from '~/services/vocabulary-service/vocabulary-service'; export type ServiceRepository = ReturnType; @@ -47,6 +49,7 @@ export const createServices = (config: Config, actions: ApiActions) => { const projectService = new ProjectService(apiClient, actions); const repositoriesService = new RepositoriesService(apiClient, actions); const userService = new UserService(apiClient, actions); + const virtualMachineService = new VirtualMachinesService(apiClient, actions); const workflowService = new WorkflowService(apiClient, actions); const ancestorsService = new AncestorService(groupsService, userService); @@ -56,6 +59,7 @@ export const createServices = (config: Config, actions: ApiActions) => { const favoriteService = new FavoriteService(linkService, groupsService); const tagService = new TagService(linkService); const searchService = new SearchService(); + const vocabularyService = new VocabularyService(config.vocabularyUrl); return { ancestorsService, @@ -77,8 +81,10 @@ export const createServices = (config: Config, actions: ApiActions) => { searchService, tagService, userService, + virtualMachineService, webdavClient, workflowService, + vocabularyService, }; }; diff --git a/src/services/virtual-machines-service/virtual-machines-service.ts b/src/services/virtual-machines-service/virtual-machines-service.ts new file mode 100644 index 00000000..c54eff47 --- /dev/null +++ b/src/services/virtual-machines-service/virtual-machines-service.ts @@ -0,0 +1,38 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { AxiosInstance } from "axios"; +import { CommonResourceService } from "~/services/common-service/common-resource-service"; +import { VirtualMachinesResource } from '~/models/virtual-machines'; +import { ApiActions } from '~/services/api/api-actions'; + +export class VirtualMachinesService extends CommonResourceService { + constructor(serverApi: AxiosInstance, actions: ApiActions) { + super(serverApi, "virtual_machines", actions); + } + + getRequestedDate(): string { + return localStorage.getItem('requestedDate') || ''; + } + + saveRequestedDate(date: string) { + localStorage.setItem('requestedDate', date); + } + + logins(uuid: string) { + return CommonResourceService.defaultResponse( + this.serverApi + .get(`virtual_machines/${uuid}/logins`), + this.actions + ); + } + + getAllLogins() { + return CommonResourceService.defaultResponse( + this.serverApi + .get('virtual_machines/get_all_logins'), + this.actions + ); + } +} \ No newline at end of file diff --git a/src/services/vocabulary-service/vocabulary-service.ts b/src/services/vocabulary-service/vocabulary-service.ts new file mode 100644 index 00000000..57bdd7c9 --- /dev/null +++ b/src/services/vocabulary-service/vocabulary-service.ts @@ -0,0 +1,18 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import Axios from 'axios'; +import { Vocabulary } from '~/models/vocabulary'; + +export class VocabularyService { + constructor( + private url: string + ) { } + + getVocabulary() { + return Axios + .get(this.url) + .then(response => response.data); + } +} diff --git a/src/store/advanced-tab/advanced-tab.ts b/src/store/advanced-tab/advanced-tab.ts index 6ad8af22..b3c5164c 100644 --- a/src/store/advanced-tab/advanced-tab.ts +++ b/src/store/advanced-tab/advanced-tab.ts @@ -2,9 +2,9 @@ // // SPDX-License-Identifier: AGPL-3.0 +import { Dispatch } from 'redux'; import { dialogActions } from '~/store/dialog/dialog-actions'; import { RootState } from '~/store/store'; -import { Dispatch } from 'redux'; import { ResourceKind, extractUuidKind } from '~/models/resource'; import { getResource } from '~/store/resources/resources'; import { GroupContentsResourcePrefix } from '~/services/groups-service/groups-service'; @@ -15,13 +15,14 @@ import { ProjectResource } from '~/models/project'; import { ServiceRepository } from '~/services/services'; import { FilterBuilder } from '~/services/api/filter-builder'; import { RepositoryResource } from '~/models/repositories'; +import { SshKeyResource } from '~/models/ssh-key'; export const ADVANCED_TAB_DIALOG = 'advancedTabDialog'; -export interface AdvancedTabDialogData { +interface AdvancedTabDialogData { apiResponse: any; metadata: any; - uuid: string; + user: string; pythonHeader: string; pythonExample: string; cliGetHeader: string; @@ -52,41 +53,65 @@ enum RepositoryData { CREATED_AT = 'created_at' } +enum SshKeyData { + SSH_KEY = 'authorized_keys', + CREATED_AT = 'created_at' +} + +type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData; +type AdvanceResourcePrefix = GroupContentsResourcePrefix | 'repositories' | 'authorized_keys'; + export const openAdvancedTabDialog = (uuid: string, index?: number) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - const { resources } = getState(); const kind = extractUuidKind(uuid); - const data = getResource(uuid)(resources); - const repositoryData = getState().repositories.items[index!]; - if (data || repositoryData) { - if (data) { - const user = await services.userService.get(data.ownerUuid); - const metadata = await services.linkService.list({ - filters: new FilterBuilder() - .addEqual('headUuid', uuid) - .getFilters() - }); - if (kind === ResourceKind.COLLECTION) { - const dataCollection: AdvancedTabDialogData = advancedTabData(uuid, metadata, user, collectionApiResponse, data, CollectionData.COLLECTION, GroupContentsResourcePrefix.COLLECTION, CollectionData.STORAGE_CLASSES_CONFIRMED, data.storageClassesConfirmed); - dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataCollection })); - } else if (kind === ResourceKind.PROCESS) { - const dataProcess: AdvancedTabDialogData = advancedTabData(uuid, metadata, user, containerRequestApiResponse, data, ProcessData.CONTAINER_REQUEST, GroupContentsResourcePrefix.PROCESS, ProcessData.OUTPUT_NAME, data.outputName); - dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataProcess })); - } else if (kind === ResourceKind.PROJECT) { - const dataProject: AdvancedTabDialogData = advancedTabData(uuid, metadata, user, groupRequestApiResponse, data, ProjectData.GROUP, GroupContentsResourcePrefix.PROJECT, ProjectData.DELETE_AT, data.deleteAt); - dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataProject })); - } - - } else if (kind === ResourceKind.REPOSITORY) { - const dataRepository: AdvancedTabDialogData = advancedTabData(uuid, '', '', repositoryApiResponse, repositoryData, RepositoryData.REPOSITORY, 'repositories', RepositoryData.CREATED_AT, repositoryData.createdAt); - dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataRepository })); - } - } else { - dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Could not open advanced tab for this resource.", hideDuration: 2000, kind: SnackbarKind.ERROR })); + switch (kind) { + case ResourceKind.COLLECTION: + const { data: dataCollection, metadata: metaCollection, user: userCollection } = await dispatch(getDataForAdvancedTab(uuid)); + const advanceDataCollection: AdvancedTabDialogData = advancedTabData(uuid, metaCollection, userCollection, collectionApiResponse, dataCollection, CollectionData.COLLECTION, GroupContentsResourcePrefix.COLLECTION, CollectionData.STORAGE_CLASSES_CONFIRMED, dataCollection.storageClassesConfirmed); + dispatch(initAdvancedTabDialog(advanceDataCollection)); + break; + case ResourceKind.PROCESS: + const { data: dataProcess, metadata: metaProcess, user: userProcess } = await dispatch(getDataForAdvancedTab(uuid)); + const advancedDataProcess: AdvancedTabDialogData = advancedTabData(uuid, metaProcess, userProcess, containerRequestApiResponse, dataProcess, ProcessData.CONTAINER_REQUEST, GroupContentsResourcePrefix.PROCESS, ProcessData.OUTPUT_NAME, dataProcess.outputName); + dispatch(initAdvancedTabDialog(advancedDataProcess)); + break; + case ResourceKind.PROJECT: + const { data: dataProject, metadata: metaProject, user: userProject } = await dispatch(getDataForAdvancedTab(uuid)); + const advanceDataProject: AdvancedTabDialogData = advancedTabData(uuid, metaProject, userProject, groupRequestApiResponse, dataProject, ProjectData.GROUP, GroupContentsResourcePrefix.PROJECT, ProjectData.DELETE_AT, dataProject.deleteAt); + dispatch(initAdvancedTabDialog(advanceDataProject)); + break; + case ResourceKind.REPOSITORY: + const dataRepository = getState().repositories.items[index!]; + const advanceDataRepository: AdvancedTabDialogData = advancedTabData(uuid, '', '', repositoryApiResponse, dataRepository, RepositoryData.REPOSITORY, 'repositories', RepositoryData.CREATED_AT, dataRepository.createdAt); + dispatch(initAdvancedTabDialog(advanceDataRepository)); + break; + case ResourceKind.SSH_KEY: + const dataSshKey = getState().auth.sshKeys[index!]; + const advanceDataSshKey: AdvancedTabDialogData = advancedTabData(uuid, '', '', sshKeyApiResponse, dataSshKey, SshKeyData.SSH_KEY, 'authorized_keys', SshKeyData.CREATED_AT, dataSshKey.createdAt); + dispatch(initAdvancedTabDialog(advanceDataSshKey)); + break; + default: + dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Could not open advanced tab for this resource.", hideDuration: 2000, kind: SnackbarKind.ERROR })); } }; -const advancedTabData = (uuid: string, metadata: any, user: any, apiResponseKind: any, data: any, resourceKind: CollectionData | ProcessData | ProjectData | RepositoryData, resourcePrefix: GroupContentsResourcePrefix | 'repositories', resourceKindProperty: CollectionData | ProcessData | ProjectData | RepositoryData, property: any) => { +const getDataForAdvancedTab = (uuid: string) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const { resources } = getState(); + const data = getResource(uuid)(resources); + const metadata = await services.linkService.list({ + filters: new FilterBuilder() + .addEqual('headUuid', uuid) + .getFilters() + }); + const user = metadata.itemsAvailable && await services.userService.get(metadata.items[0].tailUuid || ''); + return { data, metadata, user }; + }; + +const initAdvancedTabDialog = (data: AdvancedTabDialogData) => dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data }); + +const advancedTabData = (uuid: string, metadata: any, user: any, apiResponseKind: any, data: any, resourceKind: AdvanceResourceKind, + resourcePrefix: AdvanceResourcePrefix, resourceKindProperty: AdvanceResourceKind, property: any) => { return { uuid, user, @@ -109,7 +134,7 @@ const pythonHeader = (resourceKind: string) => const pythonExample = (uuid: string, resourcePrefix: string) => { const pythonExample = `import arvados - x = arvados.api().${resourcePrefix}().get(uuid='${uuid}').execute()`; +x = arvados.api().${resourcePrefix}().get(uuid='${uuid}').execute()`; return pythonExample; }; @@ -119,7 +144,7 @@ const cliGetHeader = (resourceKind: string) => const cliGetExample = (uuid: string, resourceKind: string) => { const cliGetExample = `arv ${resourceKind} get \\ - --uuid ${uuid}`; + --uuid ${uuid}`; return cliGetExample; }; @@ -128,9 +153,9 @@ const cliUpdateHeader = (resourceKind: string, resourceName: string) => `An example arv command to update the "${resourceName}" attribute for the current ${resourceKind}:`; const cliUpdateExample = (uuid: string, resourceKind: string, resource: string | string[], resourceName: string) => { - const CLIUpdateCollectionExample = `arv ${resourceKind} update \\ - --uuid ${uuid} \\ - --${resourceKind} '{"${resourceName}":${resource}}'`; + const CLIUpdateCollectionExample = `arv ${resourceKind} update \\ + --uuid ${uuid} \\ + --${resourceKind} '{"${resourceName}":${resource}}'`; return CLIUpdateCollectionExample; }; @@ -140,10 +165,10 @@ const curlHeader = (resourceKind: string, resource: string) => const curlExample = (uuid: string, resourcePrefix: string, resource: string | string[], resourceKind: string, resourceName: string) => { const curlExample = `curl -X PUT \\ - -H "Authorization: OAuth2 $ARVADOS_API_TOKEN" \\ - --data-urlencode ${resourceKind}@/dev/stdin \\ - https://$ARVADOS_API_HOST/arvados/v1/${resourcePrefix}/${uuid} \\ - < { "name": ${stringify(name)}, "created_at": "${createdAt}"`; + return response; +}; + +const sshKeyApiResponse = (apiResponse: SshKeyResource) => { + const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name, authorizedUserUuid, expiresAt } = apiResponse; + const response = `"uuid": "${uuid}", +"owner_uuid": "${ownerUuid}", +"authorized_user_uuid": "${authorizedUserUuid}", +"modified_by_client_uuid": ${stringify(modifiedByClientUuid)}, +"modified_by_user_uuid": ${stringify(modifiedByUserUuid)}, +"modified_at": ${stringify(modifiedAt)}, +"name": ${stringify(name)}, +"created_at": "${createdAt}", +"expires_at": "${expiresAt}"`; return response; }; \ No newline at end of file diff --git a/src/store/auth/auth-actions.test.ts b/src/store/auth/auth-actions.test.ts index a1cd7f4f..3d6913a6 100644 --- a/src/store/auth/auth-actions.test.ts +++ b/src/store/auth/auth-actions.test.ts @@ -10,7 +10,8 @@ import { USER_FIRST_NAME_KEY, USER_LAST_NAME_KEY, USER_OWNER_UUID_KEY, - USER_UUID_KEY + USER_UUID_KEY, + USER_IS_ADMIN } from "~/services/auth-service/auth-service"; import 'jest-localstorage-mock'; @@ -42,17 +43,20 @@ describe('auth-actions', () => { localStorage.setItem(USER_LAST_NAME_KEY, "Doe"); localStorage.setItem(USER_UUID_KEY, "uuid"); localStorage.setItem(USER_OWNER_UUID_KEY, "ownerUuid"); + localStorage.setItem(USER_IS_ADMIN, JSON.stringify(false)); store.dispatch(initAuth()); expect(store.getState().auth).toEqual({ apiToken: "token", + sshKeys: [], user: { email: "test@test.com", firstName: "John", lastName: "Doe", uuid: "uuid", - ownerUuid: "ownerUuid" + ownerUuid: "ownerUuid", + isAdmin: false } }); }); diff --git a/src/store/auth/auth-reducer.test.ts b/src/store/auth/auth-reducer.test.ts index 592191ca..b9f768f1 100644 --- a/src/store/auth/auth-reducer.test.ts +++ b/src/store/auth/auth-reducer.test.ts @@ -31,12 +31,14 @@ describe('auth-reducer', () => { uuid: "uuid", ownerUuid: "ownerUuid", identityUrl: "identityUrl", - prefs: {} + prefs: {}, + isAdmin: false }; const state = reducer(initialState, authActions.INIT({ user, token: "token" })); expect(state).toEqual({ apiToken: "token", - user + user, + sshKeys: [] }); }); @@ -61,7 +63,8 @@ describe('auth-reducer', () => { uuid: "uuid", ownerUuid: "ownerUuid", identityUrl: "identityUrl", - prefs: {} + prefs: {}, + isAdmin: false }; const state = reducer(initialState, authActions.USER_DETAILS_SUCCESS(user)); @@ -74,6 +77,7 @@ describe('auth-reducer', () => { lastName: "Doe", uuid: "uuid", ownerUuid: "ownerUuid", + isAdmin: false } }); }); diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts index 0a6b5a82..5631a5e8 100644 --- a/src/store/context-menu/context-menu-actions.ts +++ b/src/store/context-menu/context-menu-actions.ts @@ -132,10 +132,10 @@ export const openProcessContextMenu = (event: React.MouseEvent, pro (dispatch: Dispatch, getState: () => RootState) => { const resource = { uuid: process.containerRequest.uuid, - ownerUuid: '', + ownerUuid: process.containerRequest.ownerUuid, kind: ResourceKind.PROCESS, - name: '', - description: '', + name: process.containerRequest.name, + description: process.containerRequest.description, menuKind: ContextMenuKind.PROCESS }; dispatch(openContextMenu(event, resource)); diff --git a/src/store/current-token-dialog/current-token-dialog-actions.tsx b/src/store/current-token-dialog/current-token-dialog-actions.tsx index 030b18e2..fe8186b7 100644 --- a/src/store/current-token-dialog/current-token-dialog-actions.tsx +++ b/src/store/current-token-dialog/current-token-dialog-actions.tsx @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0 import { dialogActions } from "~/store/dialog/dialog-actions"; -import { getProperty } from '../properties/properties'; +import { getProperty } from '~/store/properties/properties'; import { propertiesActions } from '~/store/properties/properties-actions'; import { RootState } from '~/store/store'; diff --git a/src/store/data-explorer/data-explorer-action.ts b/src/store/data-explorer/data-explorer-action.ts index a58d20ed..7797ae6c 100644 --- a/src/store/data-explorer/data-explorer-action.ts +++ b/src/store/data-explorer/data-explorer-action.ts @@ -3,14 +3,14 @@ // SPDX-License-Identifier: AGPL-3.0 import { unionize, ofType, UnionOf } from "~/common/unionize"; -import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters"; import { DataColumns } from "~/components/data-table/data-table"; +import { DataTableFilters } from '~/components/data-table-filters/data-table-filters-tree'; export const dataExplorerActions = unionize({ RESET_PAGINATION: ofType<{ id: string }>(), REQUEST_ITEMS: ofType<{ id: string }>(), SET_COLUMNS: ofType<{ id: string, columns: DataColumns }>(), - SET_FILTERS: ofType<{ id: string, columnName: string, filters: DataTableFilterItem[] }>(), + SET_FILTERS: ofType<{ id: string, columnName: string, filters: DataTableFilters }>(), SET_ITEMS: ofType<{ id: string, items: any[], page: number, rowsPerPage: number, itemsAvailable: number }>(), SET_PAGE: ofType<{ id: string, page: number }>(), SET_ROWS_PER_PAGE: ofType<{ id: string, rowsPerPage: number }>(), @@ -28,7 +28,7 @@ export const bindDataExplorerActions = (id: string) => ({ dataExplorerActions.REQUEST_ITEMS({ id }), SET_COLUMNS: (payload: { columns: DataColumns }) => dataExplorerActions.SET_COLUMNS({ ...payload, id }), - SET_FILTERS: (payload: { columnName: string, filters: DataTableFilterItem[] }) => + SET_FILTERS: (payload: { columnName: string, filters: DataTableFilters }) => dataExplorerActions.SET_FILTERS({ ...payload, id }), SET_ITEMS: (payload: { items: any[], page: number, rowsPerPage: number, itemsAvailable: number }) => dataExplorerActions.SET_ITEMS({ ...payload, id }), diff --git a/src/store/data-explorer/data-explorer-middleware-service.ts b/src/store/data-explorer/data-explorer-middleware-service.ts index 934af7be..80ab514c 100644 --- a/src/store/data-explorer/data-explorer-middleware-service.ts +++ b/src/store/data-explorer/data-explorer-middleware-service.ts @@ -5,9 +5,10 @@ import { Dispatch, MiddlewareAPI } from "redux"; import { RootState } from "../store"; import { DataColumns } from "~/components/data-table/data-table"; -import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters"; import { DataExplorer } from './data-explorer-reducer'; import { ListResults } from '~/services/common-service/common-resource-service'; +import { createTree } from "~/models/tree"; +import { DataTableFilters } from "~/components/data-table-filters/data-table-filters-tree"; export abstract class DataExplorerMiddlewareService { protected readonly id: string; @@ -20,20 +21,19 @@ export abstract class DataExplorerMiddlewareService { return this.id; } - public getColumnFilters(columns: DataColumns, columnName: string): F[] { - const column = columns.find(c => c.name === columnName); - return column ? column.filters.filter(f => f.selected) : []; + public getColumnFilters(columns: DataColumns, columnName: string): DataTableFilters { + return getDataExplorerColumnFilters(columns, columnName); } abstract requestItems(api: MiddlewareAPI): void; } -export const getDataExplorerColumnFilters = (columns: DataColumns, columnName: string): F[] => { +export const getDataExplorerColumnFilters = (columns: DataColumns, columnName: string): DataTableFilters => { const column = columns.find(c => c.name === columnName); - return column ? column.filters.filter(f => f.selected) : []; + return column ? column.filters : createTree(); }; -export const dataExplorerToListParams = (dataExplorer: DataExplorer) => ({ +export const dataExplorerToListParams = (dataExplorer: DataExplorer) => ({ limit: dataExplorer.rowsPerPage, offset: dataExplorer.page * dataExplorer.rowsPerPage, }); diff --git a/src/store/data-explorer/data-explorer-middleware.test.ts b/src/store/data-explorer/data-explorer-middleware.test.ts index 814d5855..00931bf8 100644 --- a/src/store/data-explorer/data-explorer-middleware.test.ts +++ b/src/store/data-explorer/data-explorer-middleware.test.ts @@ -8,6 +8,8 @@ import { MiddlewareAPI } from "redux"; import { DataColumns } from "~/components/data-table/data-table"; import { dataExplorerActions } from "./data-explorer-action"; import { SortDirection } from "~/components/data-table/data-column"; +import { createTree } from '~/models/tree'; +import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters-tree"; describe("DataExplorerMiddleware", () => { @@ -20,7 +22,7 @@ describe("DataExplorerMiddleware", () => { selected: true, configurable: false, sortDirection: SortDirection.NONE, - filters: [], + filters: createTree(), render: jest.fn() }], requestItems: jest.fn(), @@ -48,7 +50,7 @@ describe("DataExplorerMiddleware", () => { selected: true, configurable: false, sortDirection: SortDirection.NONE, - filters: [], + filters: createTree(), render: jest.fn() }], requestItems: jest.fn(), @@ -115,7 +117,7 @@ describe("DataExplorerMiddleware", () => { }; const next = jest.fn(); const middleware = dataExplorerMiddleware(service)(api)(next); - middleware(dataExplorerActions.SET_FILTERS({ id: service.getId(), columnName: "", filters: [] })); + middleware(dataExplorerActions.SET_FILTERS({ id: service.getId(), columnName: "", filters: createTree() })); expect(api.dispatch).toHaveBeenCalledTimes(2); }); diff --git a/src/store/data-explorer/data-explorer-reducer.ts b/src/store/data-explorer/data-explorer-reducer.ts index 1657ab70..613bf278 100644 --- a/src/store/data-explorer/data-explorer-reducer.ts +++ b/src/store/data-explorer/data-explorer-reducer.ts @@ -4,8 +4,8 @@ import { DataColumn, toggleSortDirection, resetSortDirection, SortDirection } from "~/components/data-table/data-column"; import { dataExplorerActions, DataExplorerAction } from "./data-explorer-action"; -import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters"; import { DataColumns } from "~/components/data-table/data-table"; +import { DataTableFilters } from "~/components/data-table-filters/data-table-filters-tree"; export interface DataExplorer { columns: DataColumns; @@ -103,7 +103,7 @@ const toggleColumn = (columnName: string) => ? { ...column, selected: !column.selected } : column; -const setFilters = (columnName: string, filters: DataTableFilterItem[]) => +const setFilters = (columnName: string, filters: DataTableFilters) => (column: DataColumn) => column.name === columnName ? { ...column, filters } : column; diff --git a/src/store/favorite-panel/favorite-panel-middleware-service.ts b/src/store/favorite-panel/favorite-panel-middleware-service.ts index d7d54ded..87f49f34 100644 --- a/src/store/favorite-panel/favorite-panel-middleware-service.ts +++ b/src/store/favorite-panel/favorite-panel-middleware-service.ts @@ -2,8 +2,8 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { DataExplorerMiddlewareService } from "../data-explorer/data-explorer-middleware-service"; -import { FavoritePanelColumnNames, FavoritePanelFilter } from "~/views/favorite-panel/favorite-panel"; +import { DataExplorerMiddlewareService } from "~/store/data-explorer/data-explorer-middleware-service"; +import { FavoritePanelColumnNames } from "~/views/favorite-panel/favorite-panel"; import { RootState } from "../store"; import { DataColumns } from "~/components/data-table/data-table"; import { ServiceRepository } from "~/services/services"; @@ -21,6 +21,8 @@ import { progressIndicatorActions } from '~/store/progress-indicator/progress-in import { getDataExplorer } from "~/store/data-explorer/data-explorer-reducer"; import { loadMissingProcessesInformation } from "~/store/project-panel/project-panel-middleware-service"; import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer"; +import { getDataExplorerColumnFilters } from '~/store/data-explorer/data-explorer-middleware-service'; +import { serializeSimpleObjectTypeFilters } from '../resource-type-filters/resource-type-filters'; export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareService { constructor(private services: ServiceRepository, id: string) { @@ -32,9 +34,10 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic if (!dataExplorer) { api.dispatch(favoritesPanelDataExplorerIsNotSet()); } else { - const columns = dataExplorer.columns as DataColumns; + const columns = dataExplorer.columns as DataColumns; const sortColumn = getSortColumn(dataExplorer); - const typeFilters = this.getColumnFilters(columns, FavoritePanelColumnNames.TYPE); + const typeFilters = serializeSimpleObjectTypeFilters(getDataExplorerColumnFilters(columns, FavoritePanelColumnNames.TYPE)); + const linkOrder = new OrderBuilder(); const contentOrder = new OrderBuilder(); @@ -59,9 +62,10 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic linkOrder: linkOrder.getOrder(), contentOrder: contentOrder.getOrder(), filters: new FilterBuilder() - .addIsA("headUuid", typeFilters.map(filter => filter.type)) .addILike("name", dataExplorer.searchValue) - .getFilters() + .addIsA("headUuid", typeFilters) + .getFilters(), + }); api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); api.dispatch(resourcesActions.SET_RESOURCES(response.items)); diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts index 0b6713bf..80a7f213 100644 --- a/src/store/navigation/navigation-action.ts +++ b/src/store/navigation/navigation-action.ts @@ -62,6 +62,8 @@ export const navigateToRunProcess = push(Routes.RUN_PROCESS); export const navigateToSearchResults = push(Routes.SEARCH_RESULTS); +export const navigateToVirtualMachines = push(Routes.VIRTUAL_MACHINES); + export const navigateToRepositories = push(Routes.REPOSITORIES); export const navigateToSshKeys= push(Routes.SSH_KEYS); diff --git a/src/store/processes/process-copy-actions.ts b/src/store/processes/process-copy-actions.ts index cd3fe21c..01387852 100644 --- a/src/store/processes/process-copy-actions.ts +++ b/src/store/processes/process-copy-actions.ts @@ -9,7 +9,7 @@ import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree import { RootState } from '~/store/store'; import { ServiceRepository } from '~/services/services'; import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog'; -import { getProcess, ProcessStatus, getProcessStatus } from '~/store/processes/process'; +import { getProcess } from '~/store/processes/process'; import { snackbarActions } from '~/store/snackbar/snackbar-actions'; import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions'; @@ -19,16 +19,11 @@ export const openCopyProcessDialog = (resource: { name: string, uuid: string }) (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { const process = getProcess(resource.uuid)(getState().resources); if (process) { - const processStatus = getProcessStatus(process); - if (processStatus === ProcessStatus.DRAFT) { - dispatch(resetPickerProjectTree()); - dispatch(initProjectsTreePicker(PROCESS_COPY_FORM_NAME)); - const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, uuid: resource.uuid, ownerUuid: '' }; - dispatch(initialize(PROCESS_COPY_FORM_NAME, initialData)); - dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_COPY_FORM_NAME, data: {} })); - } else { - dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'You can copy only draft processes.', hideDuration: 2000 })); - } + dispatch(resetPickerProjectTree()); + dispatch(initProjectsTreePicker(PROCESS_COPY_FORM_NAME)); + const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, uuid: resource.uuid, ownerUuid: '' }; + dispatch(initialize(PROCESS_COPY_FORM_NAME, initialData)); + dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_COPY_FORM_NAME, data: {} })); } else { dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process not found', hideDuration: 2000 })); } @@ -39,9 +34,8 @@ export const copyProcess = (resource: CopyFormDialogData) => dispatch(startSubmit(PROCESS_COPY_FORM_NAME)); try { const process = await services.containerRequestService.get(resource.uuid); - const uuidKey = 'uuid'; - delete process[uuidKey]; - await services.containerRequestService.create({ ...process, ownerUuid: resource.ownerUuid, name: resource.name }); + const { kind, containerImage, outputPath, outputName, containerCountMax, command, properties, requestingContainerUuid, mounts, runtimeConstraints, schedulingParameters, environment, cwd, outputTtl, priority, expiresAt, useExisting, filters } = process; + await services.containerRequestService.create({ command, containerImage, outputPath, ownerUuid: resource.ownerUuid, name: resource.name, kind, outputName, containerCountMax, properties, requestingContainerUuid, mounts, runtimeConstraints, schedulingParameters, environment, cwd, outputTtl, priority, expiresAt, useExisting, filters }); dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_COPY_FORM_NAME })); return process; } catch (e) { diff --git a/src/store/processes/process-move-actions.ts b/src/store/processes/process-move-actions.ts index edba5a85..7e65bcca 100644 --- a/src/store/processes/process-move-actions.ts +++ b/src/store/processes/process-move-actions.ts @@ -12,7 +12,7 @@ import { snackbarActions } from '~/store/snackbar/snackbar-actions'; import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog'; import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions'; import { projectPanelActions } from '~/store/project-panel/project-panel-action'; -import { getProcess, getProcessStatus, ProcessStatus } from '~/store/processes/process'; +import { getProcess } from '~/store/processes/process'; import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions'; export const PROCESS_MOVE_FORM_NAME = 'processMoveFormName'; @@ -21,15 +21,10 @@ export const openMoveProcessDialog = (resource: { name: string, uuid: string }) (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { const process = getProcess(resource.uuid)(getState().resources); if (process) { - const processStatus = getProcessStatus(process); - if (processStatus === ProcessStatus.DRAFT) { - dispatch(resetPickerProjectTree()); - dispatch(initProjectsTreePicker(PROCESS_MOVE_FORM_NAME)); - dispatch(initialize(PROCESS_MOVE_FORM_NAME, resource)); - dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_MOVE_FORM_NAME, data: {} })); - } else { - dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'You can move only draft processes.', hideDuration: 2000 })); - } + dispatch(resetPickerProjectTree()); + dispatch(initProjectsTreePicker(PROCESS_MOVE_FORM_NAME)); + dispatch(initialize(PROCESS_MOVE_FORM_NAME, resource)); + dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_MOVE_FORM_NAME, data: {} })); } else { dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process not found', hideDuration: 2000 })); } @@ -40,7 +35,7 @@ export const moveProcess = (resource: MoveToFormDialogData) => dispatch(startSubmit(PROCESS_MOVE_FORM_NAME)); try { const process = await services.containerRequestService.get(resource.uuid); - await services.containerRequestService.update(resource.uuid, { ...process, ownerUuid: resource.ownerUuid }); + await services.containerRequestService.update(resource.uuid, { ownerUuid: resource.ownerUuid }); dispatch(projectPanelActions.REQUEST_ITEMS()); dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_MOVE_FORM_NAME })); return process; @@ -48,8 +43,6 @@ export const moveProcess = (resource: MoveToFormDialogData) => const error = getCommonResourceServiceError(e); if (error === CommonResourceServiceError.UNIQUE_VIOLATION) { dispatch(stopSubmit(PROCESS_MOVE_FORM_NAME, { ownerUuid: 'A process with the same name already exists in the target project.' })); - } else if (error === CommonResourceServiceError.MODIFYING_CONTAINER_REQUEST_FINAL_STATE) { - dispatch(stopSubmit(PROCESS_MOVE_FORM_NAME, { ownerUuid: 'You can move only draft processes.' })); } else { dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_MOVE_FORM_NAME })); dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not move the process.', hideDuration: 2000 })); diff --git a/src/store/processes/process-update-actions.ts b/src/store/processes/process-update-actions.ts index 92cf032f..2063f113 100644 --- a/src/store/processes/process-update-actions.ts +++ b/src/store/processes/process-update-actions.ts @@ -34,8 +34,7 @@ export const updateProcess = (resource: ProcessUpdateFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { dispatch(startSubmit(PROCESS_UPDATE_FORM_NAME)); try { - const process = await services.containerRequestService.get(resource.uuid); - const updatedProcess = await services.containerRequestService.update(resource.uuid, { ...process, name: resource.name }); + const updatedProcess = await services.containerRequestService.update(resource.uuid, { name: resource.name }); dispatch(projectPanelActions.REQUEST_ITEMS()); dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_UPDATE_FORM_NAME })); return updatedProcess; @@ -43,8 +42,6 @@ export const updateProcess = (resource: ProcessUpdateFormDialogData) => const error = getCommonResourceServiceError(e); if (error === CommonResourceServiceError.UNIQUE_VIOLATION) { dispatch(stopSubmit(PROCESS_UPDATE_FORM_NAME, { name: 'Process with the same name already exists.' })); - } else if (error === CommonResourceServiceError.MODIFYING_CONTAINER_REQUEST_FINAL_STATE) { - dispatch(stopSubmit(PROCESS_UPDATE_FORM_NAME, { name: 'You cannot modified in "Final" state.' })); } else { dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_UPDATE_FORM_NAME })); dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not update the process.', hideDuration: 2000 })); diff --git a/src/store/project-panel/project-panel-action.ts b/src/store/project-panel/project-panel-action.ts index ef720923..21598fad 100644 --- a/src/store/project-panel/project-panel-action.ts +++ b/src/store/project-panel/project-panel-action.ts @@ -2,10 +2,9 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { bindDataExplorerActions } from "../data-explorer/data-explorer-action"; -import { propertiesActions } from "~/store/properties/properties-actions"; import { Dispatch } from 'redux'; -import { ServiceRepository } from "~/services/services"; +import { bindDataExplorerActions } from "~/store/data-explorer/data-explorer-action"; +import { propertiesActions } from "~/store/properties/properties-actions"; import { RootState } from '~/store/store'; import { getProperty } from "~/store/properties/properties"; @@ -15,7 +14,7 @@ export const IS_PROJECT_PANEL_TRASHED = 'isProjectPanelTrashed'; export const projectPanelActions = bindDataExplorerActions(PROJECT_PANEL_ID); export const openProjectPanel = (projectUuid: string) => - (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + (dispatch: Dispatch) => { dispatch(propertiesActions.SET_PROPERTY({ key: PROJECT_PANEL_CURRENT_UUID, value: projectUuid })); dispatch(projectPanelActions.REQUEST_ITEMS()); }; diff --git a/src/store/project-panel/project-panel-middleware-service.ts b/src/store/project-panel/project-panel-middleware-service.ts index 458444f3..36672e99 100644 --- a/src/store/project-panel/project-panel-middleware-service.ts +++ b/src/store/project-panel/project-panel-middleware-service.ts @@ -14,7 +14,7 @@ import { DataColumns } from "~/components/data-table/data-table"; import { ServiceRepository } from "~/services/services"; import { SortDirection } from "~/components/data-table/data-column"; import { OrderBuilder, OrderDirection } from "~/services/api/order-builder"; -import { FilterBuilder } from "~/services/api/filter-builder"; +import { FilterBuilder, joinFilters } from "~/services/api/filter-builder"; import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service"; import { updateFavorites } from "../favorites/favorites-actions"; import { PROJECT_PANEL_CURRENT_UUID, IS_PROJECT_PANEL_TRASHED, projectPanelActions } from './project-panel-action'; @@ -32,6 +32,7 @@ import { getResource } from "~/store/resources/resources"; import { CollectionResource } from "~/models/collection"; import { resourcesDataActions } from "~/store/resources-data/resources-data-actions"; import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer"; +import { serializeResourceTypeFilters } from '../resource-type-filters/resource-type-filters'; export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService { constructor(private services: ServiceRepository, id: string) { @@ -115,15 +116,22 @@ export const getParams = (dataExplorer: DataExplorer, isProjectTrashed: boolean) }); export const getFilters = (dataExplorer: DataExplorer) => { - const columns = dataExplorer.columns as DataColumns; - const typeFilters = getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.TYPE); - const statusFilters = getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.STATUS); - return new FilterBuilder() - .addIsA("uuid", typeFilters.map(f => f.type)) + const columns = dataExplorer.columns as DataColumns; + const typeFilters = serializeResourceTypeFilters(getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.TYPE)); + + // TODO: Extract group contents name filter + const nameFilters = new FilterBuilder() .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION) .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS) .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT) .getFilters(); + + return joinFilters( + typeFilters, + nameFilters, + ); + // TODO: Restore process status filters + // const statusFilters = getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.STATUS); }; export const getOrder = (dataExplorer: DataExplorer) => { diff --git a/src/store/resource-type-filters/resource-type-filters.test.ts b/src/store/resource-type-filters/resource-type-filters.test.ts new file mode 100644 index 00000000..02f017ed --- /dev/null +++ b/src/store/resource-type-filters/resource-type-filters.test.ts @@ -0,0 +1,50 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { getInitialResourceTypeFilters, serializeResourceTypeFilters, ObjectTypeFilter, CollectionTypeFilter } from './resource-type-filters'; +import { ResourceKind } from '~/models/resource'; +import { deselectNode } from '~/models/tree'; +import { pipe } from 'lodash/fp'; + +describe("serializeResourceTypeFilters", () => { + it("should serialize all filters", () => { + const filters = getInitialResourceTypeFilters(); + const serializedFilters = serializeResourceTypeFilters(filters); + expect(serializedFilters) + .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.PROCESS}","${ResourceKind.COLLECTION}"]]`); + }); + + it("should serialize all but collection filters", () => { + const filters = deselectNode(ObjectTypeFilter.COLLECTION)(getInitialResourceTypeFilters()); + const serializedFilters = serializeResourceTypeFilters(filters); + expect(serializedFilters) + .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.PROCESS}"]]`); + }); + + it("should serialize output collections and projects", () => { + const filters = pipe( + () => getInitialResourceTypeFilters(), + deselectNode(ObjectTypeFilter.PROCESS), + deselectNode(CollectionTypeFilter.GENERAL_COLLECTION), + deselectNode(CollectionTypeFilter.LOG_COLLECTION), + )(); + + const serializedFilters = serializeResourceTypeFilters(filters); + expect(serializedFilters) + .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.COLLECTION}"]],["collections.properties.type","in",["output"]]`); + }); + + it("should serialize general and log collections", () => { + const filters = pipe( + () => getInitialResourceTypeFilters(), + deselectNode(ObjectTypeFilter.PROJECT), + deselectNode(ObjectTypeFilter.PROCESS), + deselectNode(CollectionTypeFilter.OUTPUT_COLLECTION) + )(); + + const serializedFilters = serializeResourceTypeFilters(filters); + expect(serializedFilters) + .toEqual(`["uuid","is_a",["${ResourceKind.COLLECTION}"]],["collections.properties.type","not in",["output"]]`); + }); +}); diff --git a/src/store/resource-type-filters/resource-type-filters.ts b/src/store/resource-type-filters/resource-type-filters.ts new file mode 100644 index 00000000..78777be1 --- /dev/null +++ b/src/store/resource-type-filters/resource-type-filters.ts @@ -0,0 +1,141 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { difference, pipe, values, includes, __ } from 'lodash/fp'; +import { createTree, setNode, TreeNodeStatus, TreeNode, Tree } from '~/models/tree'; +import { DataTableFilterItem, DataTableFilters } from '~/components/data-table-filters/data-table-filters-tree'; +import { ResourceKind } from '~/models/resource'; +import { FilterBuilder } from '~/services/api/filter-builder'; +import { getSelectedNodes } from '~/models/tree'; +import { CollectionType } from '~/models/collection'; +import { GroupContentsResourcePrefix } from '~/services/groups-service/groups-service'; + +export enum ObjectTypeFilter { + PROJECT = 'Project', + PROCESS = 'Process', + COLLECTION = 'Data Collection', +} + +export enum CollectionTypeFilter { + GENERAL_COLLECTION = 'General', + OUTPUT_COLLECTION = 'Output', + LOG_COLLECTION = 'Log', +} + +const initFilter = (name: string, parent = '') => + setNode({ + id: name, + value: { name }, + parent, + children: [], + active: false, + selected: true, + expanded: false, + status: TreeNodeStatus.LOADED, + }); + +export const getSimpleObjectTypeFilters = pipe( + (): DataTableFilters => createTree(), + initFilter(ObjectTypeFilter.PROJECT), + initFilter(ObjectTypeFilter.PROCESS), + initFilter(ObjectTypeFilter.COLLECTION), +); + +export const getInitialResourceTypeFilters = pipe( + (): DataTableFilters => createTree(), + initFilter(ObjectTypeFilter.PROJECT), + initFilter(ObjectTypeFilter.PROCESS), + initFilter(ObjectTypeFilter.COLLECTION), + initFilter(CollectionTypeFilter.GENERAL_COLLECTION, ObjectTypeFilter.COLLECTION), + initFilter(CollectionTypeFilter.OUTPUT_COLLECTION, ObjectTypeFilter.COLLECTION), + initFilter(CollectionTypeFilter.LOG_COLLECTION, ObjectTypeFilter.COLLECTION), +); + + +const createFiltersBuilder = (filters: DataTableFilters) => + ({ fb: new FilterBuilder(), selectedFilters: getSelectedNodes(filters) }); + +const getMatchingFilters = (values: string[], filters: TreeNode[]) => + filters + .map(f => f.id) + .filter(includes(__, values)); + +const objectTypeToResourceKind = (type: ObjectTypeFilter) => { + switch (type) { + case ObjectTypeFilter.PROJECT: + return ResourceKind.PROJECT; + case ObjectTypeFilter.PROCESS: + return ResourceKind.PROCESS; + case ObjectTypeFilter.COLLECTION: + return ResourceKind.COLLECTION; + } +}; + +const serializeObjectTypeFilters = ({ fb, selectedFilters }: ReturnType) => { + const collectionFilters = getMatchingFilters(values(CollectionTypeFilter), selectedFilters); + const typeFilters = pipe( + () => new Set(getMatchingFilters(values(ObjectTypeFilter), selectedFilters)), + set => collectionFilters.length > 0 + ? set.add(ObjectTypeFilter.COLLECTION) + : set, + set => Array.from(set) + )(); + + return { + fb: typeFilters.length > 0 + ? fb.addIsA('uuid', typeFilters.map(objectTypeToResourceKind)) + : fb, + selectedFilters, + }; +}; + +const collectionTypeToPropertyValue = (type: CollectionTypeFilter) => { + switch (type) { + case CollectionTypeFilter.GENERAL_COLLECTION: + return CollectionType.GENERAL; + case CollectionTypeFilter.OUTPUT_COLLECTION: + return CollectionType.OUTPUT; + case CollectionTypeFilter.LOG_COLLECTION: + return CollectionType.LOG; + } +}; + +const serializeCollectionTypeFilters = ({ fb, selectedFilters }: ReturnType) => pipe( + () => getMatchingFilters(values(CollectionTypeFilter), selectedFilters), + filters => filters.map(collectionTypeToPropertyValue), + mappedFilters => ({ + fb: buildCollectiomTypeFilters({ fb, filters: mappedFilters }), + selectedFilters + }) +)(); + +const COLLECTION_TYPES = values(CollectionType); + +const NON_GENERAL_COLLECTION_TYPES = difference(COLLECTION_TYPES, [CollectionType.GENERAL]); + +const COLLECTION_PROPERTIES_PREFIX = `${GroupContentsResourcePrefix.COLLECTION}.properties`; + +const buildCollectiomTypeFilters = ({ fb, filters }: { fb: FilterBuilder, filters: CollectionType[] }) => { + switch (true) { + case filters.length === 0 || filters.length === COLLECTION_TYPES.length: + return fb; + case includes(CollectionType.GENERAL, filters): + return fb.addNotIn('type', difference(NON_GENERAL_COLLECTION_TYPES, filters), COLLECTION_PROPERTIES_PREFIX); + default: + return fb.addIn('type', filters, COLLECTION_PROPERTIES_PREFIX); + } +}; + +export const serializeResourceTypeFilters = pipe( + createFiltersBuilder, + serializeObjectTypeFilters, + serializeCollectionTypeFilters, + ({ fb }) => fb.getFilters(), +); + +export const serializeSimpleObjectTypeFilters = (filters: Tree) => { + return getSelectedNodes(filters) + .map(f => f.id) + .map(objectTypeToResourceKind); +}; diff --git a/src/store/search-results-panel/search-results-panel-actions.ts b/src/store/search-results-panel/search-results-panel-actions.ts index 05da5b3e..f7dc5d45 100644 --- a/src/store/search-results-panel/search-results-panel-actions.ts +++ b/src/store/search-results-panel/search-results-panel-actions.ts @@ -6,11 +6,13 @@ import { Dispatch } from 'redux'; import { RootState } from '~/store/store'; import { ServiceRepository } from '~/services/services'; import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action'; +import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions'; export const SEARCH_RESULTS_PANEL_ID = "searchResultsPanel"; export const searchResultsPanelActions = bindDataExplorerActions(SEARCH_RESULTS_PANEL_ID); export const loadSearchResultsPanel = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(setBreadcrumbs([{ label: 'Search results' }])); dispatch(searchResultsPanelActions.REQUEST_ITEMS()); }; \ No newline at end of file diff --git a/src/store/sharing-dialog/sharing-dialog-actions.ts b/src/store/sharing-dialog/sharing-dialog-actions.ts index 0e3c76b2..37de6f8c 100644 --- a/src/store/sharing-dialog/sharing-dialog-actions.ts +++ b/src/store/sharing-dialog/sharing-dialog-actions.ts @@ -56,13 +56,18 @@ export const sendSharingInvitations = async (dispatch: Dispatch) => { const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => { const dialog = getDialog(getState().dialog, SHARING_DIALOG_NAME); - if (dialog) { dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME)); - const { items } = await permissionService.listResourcePermissions(dialog.data); - dispatch(initializePublicAccessForm(items)); - await dispatch(initializeManagementForm(items)); - dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME)); + try { + const { items } = await permissionService.listResourcePermissions(dialog.data); + dispatch(initializePublicAccessForm(items)); + await dispatch(initializeManagementForm(items)); + dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME)); + } catch (e) { + dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'You do not have access to share this item', hideDuration: 2000, kind: SnackbarKind.ERROR })); + dispatch(dialogActions.CLOSE_DIALOG({ id: SHARING_DIALOG_NAME })); + dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME)); + } } }; diff --git a/src/store/side-panel-tree/side-panel-tree-actions.ts b/src/store/side-panel-tree/side-panel-tree-actions.ts index 562f7096..09009930 100644 --- a/src/store/side-panel-tree/side-panel-tree-actions.ts +++ b/src/store/side-panel-tree/side-panel-tree-actions.ts @@ -20,7 +20,6 @@ export enum SidePanelTreeCategory { PROJECTS = 'Projects', SHARED_WITH_ME = 'Shared with me', WORKFLOWS = 'Workflows', - RECENT_OPEN = 'Recently open', FAVORITES = 'Favorites', TRASH = 'Trash' } @@ -44,7 +43,6 @@ export const getSidePanelTreeBranch = (uuid: string) => (treePicker: TreePicker) const SIDE_PANEL_CATEGORIES = [ SidePanelTreeCategory.WORKFLOWS, - SidePanelTreeCategory.RECENT_OPEN, SidePanelTreeCategory.FAVORITES, SidePanelTreeCategory.TRASH, ]; diff --git a/src/store/store.ts b/src/store/store.ts index 5e648c99..4ab0918e 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -43,6 +43,7 @@ import { searchBarReducer } from './search-bar/search-bar-reducer'; import { SEARCH_RESULTS_PANEL_ID } from '~/store/search-results-panel/search-results-panel-actions'; import { SearchResultsMiddlewareService } from './search-results-panel/search-results-middleware-service'; import { resourcesDataReducer } from "~/store/resources-data/resources-data-reducer"; +import { virtualMachinesReducer } from "~/store/virtual-machines/virtual-machines-reducer"; import { repositoriesReducer } from '~/store/repositories/repositories-reducer'; const composeEnhancers = @@ -113,5 +114,6 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({ runProcessPanel: runProcessPanelReducer, appInfo: appInfoReducer, searchBar: searchBarReducer, + virtualMachines: virtualMachinesReducer, repositories: repositoriesReducer }); diff --git a/src/store/trash-panel/trash-panel-middleware-service.ts b/src/store/trash-panel/trash-panel-middleware-service.ts index 9afc57b4..f52421a1 100644 --- a/src/store/trash-panel/trash-panel-middleware-service.ts +++ b/src/store/trash-panel/trash-panel-middleware-service.ts @@ -23,6 +23,9 @@ import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions import { updateResources } from "~/store/resources/resources-actions"; import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions"; import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer"; +import { serializeResourceTypeFilters } from '~/store//resource-type-filters/resource-type-filters'; +import { getDataExplorerColumnFilters } from '~/store/data-explorer/data-explorer-middleware-service'; +import { joinFilters } from '../../services/api/filter-builder'; export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService { constructor(private services: ServiceRepository, id: string) { @@ -31,9 +34,22 @@ export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService { async requestItems(api: MiddlewareAPI) { const dataExplorer = api.getState().dataExplorer[this.getId()]; - const columns = dataExplorer.columns as DataColumns; + const columns = dataExplorer.columns as DataColumns; const sortColumn = getSortColumn(dataExplorer); - const typeFilters = this.getColumnFilters(columns, TrashPanelColumnNames.TYPE); + + const typeFilters = serializeResourceTypeFilters(getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.TYPE)); + + const otherFilters = new FilterBuilder() + .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION) + .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS) + .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT) + .addEqual("is_trashed", true) + .getFilters(); + + const filters = joinFilters( + typeFilters, + otherFilters, + ); const order = new OrderBuilder(); @@ -55,12 +71,7 @@ export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService { .contents(userUuid, { ...dataExplorerToListParams(dataExplorer), order: order.getOrder(), - filters: new FilterBuilder() - .addIsA("uuid", typeFilters.map(f => f.type)) - .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION) - .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT) - .addEqual("is_trashed", true) - .getFilters(), + filters, recursive: true, includeTrash: true }); diff --git a/src/store/virtual-machines/virtual-machines-actions.ts b/src/store/virtual-machines/virtual-machines-actions.ts new file mode 100644 index 00000000..9bd79884 --- /dev/null +++ b/src/store/virtual-machines/virtual-machines-actions.ts @@ -0,0 +1,65 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { Dispatch } from "redux"; +import { RootState } from '~/store/store'; +import { ServiceRepository } from "~/services/services"; +import { navigateToVirtualMachines } from "../navigation/navigation-action"; +import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action'; +import { formatDate } from "~/common/formatters"; +import { unionize, ofType, UnionOf } from "~/common/unionize"; +import { VirtualMachinesLoginsResource } from '~/models/virtual-machines'; +import { FilterBuilder } from "~/services/api/filter-builder"; +import { ListResults } from "~/services/common-service/common-resource-service"; + +export const virtualMachinesActions = unionize({ + SET_REQUESTED_DATE: ofType(), + SET_VIRTUAL_MACHINES: ofType>(), + SET_LOGINS: ofType(), + SET_LINKS: ofType>() +}); + +export type VirtualMachineActions = UnionOf; + +export const VIRTUAL_MACHINES_PANEL = 'virtualMachinesPanel'; + +export const openVirtualMachines = () => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(navigateToVirtualMachines); + }; + +const loadRequestedDate = () => + (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const date = services.virtualMachineService.getRequestedDate(); + dispatch(virtualMachinesActions.SET_REQUESTED_DATE(date)); + }; + + +export const loadVirtualMachinesData = () => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(loadRequestedDate()); + const virtualMachines = await services.virtualMachineService.list(); + const virtualMachinesUuids = virtualMachines.items.map(it => it.uuid); + const links = await services.linkService.list({ + filters: new FilterBuilder() + .addIn("headUuid", virtualMachinesUuids) + .getFilters() + }); + dispatch(virtualMachinesActions.SET_VIRTUAL_MACHINES(virtualMachines)); + dispatch(virtualMachinesActions.SET_LINKS(links)); + }; + +export const saveRequestedDate = () => + (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const date = formatDate((new Date).toISOString()); + services.virtualMachineService.saveRequestedDate(date); + dispatch(loadRequestedDate()); + }; + +const virtualMachinesBindedActions = bindDataExplorerActions(VIRTUAL_MACHINES_PANEL); + +export const loadVirtualMachinesPanel = () => + (dispatch: Dispatch) => { + dispatch(virtualMachinesBindedActions.REQUEST_ITEMS()); + }; diff --git a/src/store/virtual-machines/virtual-machines-reducer.ts b/src/store/virtual-machines/virtual-machines-reducer.ts new file mode 100644 index 00000000..fa28417e --- /dev/null +++ b/src/store/virtual-machines/virtual-machines-reducer.ts @@ -0,0 +1,42 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { virtualMachinesActions, VirtualMachineActions } from '~/store/virtual-machines/virtual-machines-actions'; +import { ListResults } from '~/services/common-service/common-resource-service'; +import { VirtualMachinesLoginsResource } from '~/models/virtual-machines'; + +interface VirtualMachines { + date: string; + virtualMachines: ListResults; + logins: VirtualMachinesLoginsResource[]; + links: ListResults; +} + +const initialState: VirtualMachines = { + date: '', + virtualMachines: { + kind: '', + offset: 0, + limit: 0, + itemsAvailable: 0, + items: [] + }, + logins: [], + links: { + kind: '', + offset: 0, + limit: 0, + itemsAvailable: 0, + items: [] + } +}; + +export const virtualMachinesReducer = (state = initialState, action: VirtualMachineActions): VirtualMachines => + virtualMachinesActions.match(action, { + SET_REQUESTED_DATE: date => ({ ...state, date }), + SET_VIRTUAL_MACHINES: virtualMachines => ({ ...state, virtualMachines }), + SET_LOGINS: logins => ({ ...state, logins }), + SET_LINKS: links => ({ ...state, links }), + default: () => state + }); diff --git a/src/store/vocabulary/vocabulary-actions.ts b/src/store/vocabulary/vocabulary-actions.ts new file mode 100644 index 00000000..799cffa0 --- /dev/null +++ b/src/store/vocabulary/vocabulary-actions.ts @@ -0,0 +1,20 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { Dispatch } from 'redux'; +import { ServiceRepository } from '~/services/services'; +import { propertiesActions } from '~/store/properties/properties-actions'; +import { VOCABULARY_PROPERTY_NAME, DEFAULT_VOCABULARY } from './vocabulary-selctors'; +import { isVocabulary } from '~/models/vocabulary'; + +export const loadVocabulary = async (dispatch: Dispatch, _: {}, { vocabularyService }: ServiceRepository) => { + const vocabulary = await vocabularyService.getVocabulary(); + + dispatch(propertiesActions.SET_PROPERTY({ + key: VOCABULARY_PROPERTY_NAME, + value: isVocabulary(vocabulary) + ? vocabulary + : DEFAULT_VOCABULARY, + })); +}; diff --git a/src/store/vocabulary/vocabulary-selctors.ts b/src/store/vocabulary/vocabulary-selctors.ts new file mode 100644 index 00000000..d317cb47 --- /dev/null +++ b/src/store/vocabulary/vocabulary-selctors.ts @@ -0,0 +1,16 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { PropertiesState, getProperty } from '~/store/properties/properties'; +import { Vocabulary } from '~/models/vocabulary'; + +export const VOCABULARY_PROPERTY_NAME = 'vocabulary'; + +export const DEFAULT_VOCABULARY: Vocabulary = { + strict: false, + tags: {}, +}; + +export const getVocabulary = (state: PropertiesState) => + getProperty(VOCABULARY_PROPERTY_NAME)(state) || DEFAULT_VOCABULARY; diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts index dc54e4b8..091a8ccc 100644 --- a/src/store/workbench/workbench-actions.ts +++ b/src/store/workbench/workbench-actions.ts @@ -55,6 +55,7 @@ import { collectionPanelActions } from "~/store/collection-panel/collection-pane import { CollectionResource } from "~/models/collection"; import { searchResultsPanelActions, loadSearchResultsPanel } 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'; export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen'; @@ -393,6 +394,12 @@ export const loadSearchResults = handleFirstTimeLoad( await dispatch(loadSearchResultsPanel()); }); +export const loadVirtualMachines = handleFirstTimeLoad( + async (dispatch: Dispatch) => { + await dispatch(loadVirtualMachinesPanel()); + dispatch(setBreadcrumbs([{ label: 'Virtual Machines' }])); + }); + export const loadRepositories = handleFirstTimeLoad( async (dispatch: Dispatch) => { await dispatch(loadRepositoriesPanel()); diff --git a/src/store/workflow-panel/workflow-panel-actions.ts b/src/store/workflow-panel/workflow-panel-actions.ts index da0da54d..3d51cbb8 100644 --- a/src/store/workflow-panel/workflow-panel-actions.ts +++ b/src/store/workflow-panel/workflow-panel-actions.ts @@ -9,7 +9,9 @@ import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-act import { propertiesActions } from '~/store/properties/properties-actions'; import { getResource } from '../resources/resources'; import { getProperty } from '~/store/properties/properties'; -import { WorkflowResource } from '../../models/workflow'; +import { WorkflowResource } from '~/models/workflow'; +import { navigateToRunProcess } from '~/store/navigation/navigation-action'; +import { goToStep, runProcessPanelActions } from '~/store/run-process-panel/run-process-panel-actions'; export const WORKFLOW_PANEL_ID = "workflowPanel"; const UUID_PREFIX_PROPERTY_NAME = 'uuidPrefix'; @@ -17,8 +19,10 @@ const WORKFLOW_PANEL_DETAILS_UUID = 'workflowPanelDetailsUuid'; export const workflowPanelActions = bindDataExplorerActions(WORKFLOW_PANEL_ID); export const loadWorkflowPanel = () => - (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { dispatch(workflowPanelActions.REQUEST_ITEMS()); + const response = await services.workflowService.list(); + dispatch(runProcessPanelActions.SET_WORKFLOWS(response.items)); }; export const setUuidPrefix = (uuidPrefix: string) => @@ -28,6 +32,17 @@ export const getUuidPrefix = (state: RootState) => { return state.properties.uuidPrefix; }; +export const openRunProcess = (uuid: string) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const workflows = getState().runProcessPanel.searchWorkflows; + const workflow = workflows.find(workflow => workflow.uuid === uuid); + dispatch(navigateToRunProcess); + dispatch(runProcessPanelActions.RESET_RUN_PROCESS_PANEL()); + dispatch(goToStep(1)); + dispatch(runProcessPanelActions.SET_STEP_CHANGED(true)); + dispatch(runProcessPanelActions.SET_SELECTED_WORKFLOW(workflow!)); + }; + export const getPublicUserUuid = (state: RootState) => { const prefix = getProperty(UUID_PREFIX_PROPERTY_NAME)(state.properties); return `${prefix}-tpzed-anonymouspublic`; diff --git a/src/views-components/advanced-tab-dialog/metadataTab.tsx b/src/views-components/advanced-tab-dialog/metadataTab.tsx index 6250a7ad..bcf277c0 100644 --- a/src/views-components/advanced-tab-dialog/metadataTab.tsx +++ b/src/views-components/advanced-tab-dialog/metadataTab.tsx @@ -47,7 +47,7 @@ export const MetadataTab = withStyles(styles)((props: MetadataProps & WithStyles {it.uuid} {it.linkClass} {it.name} - {props.user ? `User: ${props.user.firstName} ${props.user.lastName}` : it.tailUuid} + {props.user && `User: ${props.user.firstName} ${props.user.lastName}`} {it.headUuid === props.uuid ? 'this' : it.headUuid} {JSON.stringify(it.properties)} diff --git a/src/views-components/context-menu/action-sets/project-action-set.ts b/src/views-components/context-menu/action-sets/project-action-set.ts index 9b8ced56..8c81e3bd 100644 --- a/src/views-components/context-menu/action-sets/project-action-set.ts +++ b/src/views-components/context-menu/action-sets/project-action-set.ts @@ -12,7 +12,6 @@ import { openProjectCreateDialog } from '~/store/projects/project-create-actions import { openProjectUpdateDialog } from '~/store/projects/project-update-actions'; import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action"; import { toggleProjectTrashed } from "~/store/trash/trash-actions"; -import { detailsPanelActions } from '~/store/details-panel/details-panel-action'; import { ShareIcon } from '~/components/icon/icon'; import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions"; import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab"; @@ -61,13 +60,13 @@ export const projectActionSet: ContextMenuActionSet = [[ dispatch(openMoveProjectDialog(resource)); } }, - { - icon: CopyIcon, - name: "Copy to project", - execute: (dispatch, resource) => { - // add code - } - }, + // { + // icon: CopyIcon, + // name: "Copy to project", + // execute: (dispatch, resource) => { + // // add code + // } + // }, { icon: DetailsIcon, name: "View details", diff --git a/src/views-components/context-menu/action-sets/repository-action-set.ts b/src/views-components/context-menu/action-sets/repository-action-set.ts index cf7fb883..22f6bee1 100644 --- a/src/views-components/context-menu/action-sets/repository-action-set.ts +++ b/src/views-components/context-menu/action-sets/repository-action-set.ts @@ -23,8 +23,8 @@ export const repositoryActionSet: ContextMenuActionSet = [[{ }, { name: "Advanced", icon: AdvancedIcon, - execute: (dispatch, { uuid, index }) => { - dispatch(openAdvancedTabDialog(uuid, index)); + execute: (dispatch, resource) => { + dispatch(openAdvancedTabDialog(resource.uuid, resource.index)); } }, { name: "Remove", diff --git a/src/views-components/context-menu/action-sets/ssh-key-action-set.ts b/src/views-components/context-menu/action-sets/ssh-key-action-set.ts index 3fa2f16f..6e86b2bc 100644 --- a/src/views-components/context-menu/action-sets/ssh-key-action-set.ts +++ b/src/views-components/context-menu/action-sets/ssh-key-action-set.ts @@ -5,6 +5,7 @@ import { ContextMenuActionSet } from "~/views-components/context-menu/context-menu-action-set"; import { AdvancedIcon, RemoveIcon, AttributesIcon } from "~/components/icon/icon"; import { openSshKeyRemoveDialog, openSshKeyAttributesDialog } from '~/store/auth/auth-action'; +import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab'; export const sshKeyActionSet: ContextMenuActionSet = [[{ name: "Attributes", @@ -16,7 +17,7 @@ export const sshKeyActionSet: ContextMenuActionSet = [[{ name: "Advanced", icon: AdvancedIcon, execute: (dispatch, { uuid, index }) => { - // ToDo + dispatch(openAdvancedTabDialog(uuid, index)); } }, { name: "Remove", diff --git a/src/views-components/context-menu/action-sets/trashed-collection-action-set.ts b/src/views-components/context-menu/action-sets/trashed-collection-action-set.ts index cefef345..ea0d1aaf 100644 --- a/src/views-components/context-menu/action-sets/trashed-collection-action-set.ts +++ b/src/views-components/context-menu/action-sets/trashed-collection-action-set.ts @@ -5,7 +5,6 @@ import { ContextMenuActionSet } from "../context-menu-action-set"; import { DetailsIcon, ProvenanceGraphIcon, AdvancedIcon, RestoreFromTrashIcon } from '~/components/icon/icon'; import { toggleCollectionTrashed } from "~/store/trash/trash-actions"; -import { detailsPanelActions } from "~/store/details-panel/details-panel-action"; import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab"; import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action'; diff --git a/src/views-components/current-token-dialog/current-token-dialog.tsx b/src/views-components/current-token-dialog/current-token-dialog.tsx index 503206a6..934be54d 100644 --- a/src/views-components/current-token-dialog/current-token-dialog.tsx +++ b/src/views-components/current-token-dialog/current-token-dialog.tsx @@ -3,12 +3,12 @@ // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; -import { Dialog, DialogActions, DialogTitle, DialogContent, WithStyles, withStyles, StyleRulesCallback, Button, Typography, Paper } from '@material-ui/core'; +import { Dialog, DialogActions, DialogTitle, DialogContent, WithStyles, withStyles, StyleRulesCallback, Button, Typography } from '@material-ui/core'; import { ArvadosTheme } from '~/common/custom-theme'; import { withDialog } from '~/store/dialog/with-dialog'; import { WithDialogProps } from '~/store/dialog/with-dialog'; import { connect } from 'react-redux'; -import { CurrentTokenDialogData, getCurrentTokenDialogData } from '~/store/current-token-dialog/current-token-dialog-actions'; +import { CurrentTokenDialogData, getCurrentTokenDialogData, CURRENT_TOKEN_DIALOG_NAME } from '~/store/current-token-dialog/current-token-dialog-actions'; import { DefaultCodeSnippet } from '~/components/default-code-snippet/default-code-snippet'; type CssRules = 'link' | 'paper' | 'button'; @@ -36,7 +36,7 @@ type CurrentTokenProps = CurrentTokenDialogData & WithDialogProps<{}> & WithStyl export const CurrentTokenDialog = withStyles(styles)( connect(getCurrentTokenDialogData)( - withDialog('currentTokenDialog')( + withDialog(CURRENT_TOKEN_DIALOG_NAME)( class extends React.Component { render() { const { classes, open, closeDialog, ...data } = this.props; diff --git a/src/views-components/data-explorer/data-explorer.tsx b/src/views-components/data-explorer/data-explorer.tsx index 59555707..710d202d 100644 --- a/src/views-components/data-explorer/data-explorer.tsx +++ b/src/views-components/data-explorer/data-explorer.tsx @@ -9,8 +9,8 @@ import { getDataExplorer } from "~/store/data-explorer/data-explorer-reducer"; import { Dispatch } from "redux"; import { dataExplorerActions } from "~/store/data-explorer/data-explorer-action"; import { DataColumn } from "~/components/data-table/data-column"; -import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters"; import { DataColumns } from "~/components/data-table/data-table"; +import { DataTableFilters } from '~/components/data-table-filters/data-table-filters-tree'; interface Props { id: string; @@ -44,7 +44,7 @@ const mapDispatchToProps = () => { dispatch(dataExplorerActions.TOGGLE_SORT({ id, columnName: column.name })); }, - onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn) => { + onFiltersChange: (filters: DataTableFilters, column: DataColumn) => { dispatch(dataExplorerActions.SET_FILTERS({ id, columnName: column.name, filters })); }, diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx index 87ba73ff..a032b3ed 100644 --- a/src/views-components/data-explorer/renderers.tsx +++ b/src/views-components/data-explorer/renderers.tsx @@ -18,8 +18,7 @@ import { ArvadosTheme } from '~/common/custom-theme'; import { compose, Dispatch } from 'redux'; import { WorkflowResource } from '~/models/workflow'; import { ResourceStatus } from '~/views/workflow-panel/workflow-panel-view'; -import { getUuidPrefix } from '~/store/workflow-panel/workflow-panel-actions'; -import { CollectionResource } from "~/models/collection"; +import { getUuidPrefix, openRunProcess } from '~/store/workflow-panel/workflow-panel-actions'; import { getResourceData } from "~/store/resources-data/resources-data"; import { openSharingDialog } from '~/store/sharing-dialog/sharing-dialog-actions'; @@ -87,12 +86,11 @@ const getPublicUuid = (uuidPrefix: string) => { return `${uuidPrefix}-tpzed-anonymouspublic`; }; -// ToDo: share onClick export const resourceShare = (dispatch: Dispatch, uuidPrefix: string, ownerUuid?: string, uuid?: string) => { const isPublic = ownerUuid === getPublicUuid(uuidPrefix); return (
- { isPublic && uuid && + {!isPublic && uuid && dispatch(openSharingDialog(uuid))}> @@ -115,6 +113,28 @@ export const ResourceShare = connect( })((props: { ownerUuid?: string, uuidPrefix: string, uuid?: string } & DispatchProp) => resourceShare(props.dispatch, props.uuidPrefix, props.ownerUuid, props.uuid)); +export const resourceRunProcess = (dispatch: Dispatch, uuid: string) => { + return ( +
+ {uuid && + + dispatch(openRunProcess(uuid))}> + + + } +
+ ); +}; + +export const ResourceRunProcess = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources); + return { + uuid: resource ? resource.uuid : '' + }; + })((props: { uuid: string } & DispatchProp) => + resourceRunProcess(props.dispatch, props.uuid)); + export const renderWorkflowStatus = (uuidPrefix: string, ownerUuid?: string) => { if (ownerUuid === getPublicUuid(uuidPrefix)) { return renderStatus(ResourceStatus.PUBLIC); diff --git a/src/views-components/main-app-bar/account-menu.tsx b/src/views-components/main-app-bar/account-menu.tsx index b93bfcfb..0aedee90 100644 --- a/src/views-components/main-app-bar/account-menu.tsx +++ b/src/views-components/main-app-bar/account-menu.tsx @@ -13,6 +13,7 @@ import { RootState } from "~/store/store"; import { openCurrentTokenDialog } from '~/store/current-token-dialog/current-token-dialog-actions'; import { openRepositoriesPanel } from "~/store/repositories/repositories-actions"; import { navigateToSshKeys, navigateToMyAccount } from '~/store/navigation/navigation-action'; +import { openVirtualMachines } from "~/store/virtual-machines/virtual-machines-actions"; interface AccountMenuProps { user?: User; @@ -32,6 +33,7 @@ export const AccountMenu = connect(mapStateToProps)( {getUserFullname(user)} + dispatch(openVirtualMachines())}>Virtual Machines dispatch(openRepositoriesPanel())}>Repositories dispatch(openCurrentTokenDialog)}>Current token dispatch(navigateToSshKeys)}>Ssh Keys diff --git a/src/views-components/main-content-bar/main-content-bar.tsx b/src/views-components/main-content-bar/main-content-bar.tsx index 741a7e00..6b84bde2 100644 --- a/src/views-components/main-content-bar/main-content-bar.tsx +++ b/src/views-components/main-content-bar/main-content-bar.tsx @@ -8,7 +8,7 @@ import { DetailsIcon } from "~/components/icon/icon"; import { Breadcrumbs } from "~/views-components/breadcrumbs/breadcrumbs"; import { connect } from 'react-redux'; import { RootState } from '~/store/store'; -import { matchWorkflowRoute, matchSshKeysRoute, matchRepositoriesRoute } from '~/routes/routes'; +import { matchWorkflowRoute, matchSshKeysRoute, matchRepositoriesRoute, matchVirtualMachineRoute } from '~/routes/routes'; import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action'; interface MainContentBarProps { @@ -22,6 +22,12 @@ const isWorkflowPath = ({ router }: RootState) => { return !!match; }; +const isVirtualMachinePath = ({ router }: RootState) => { + const pathname = router.location ? router.location.pathname : ''; + const match = matchVirtualMachineRoute(pathname); + return !!match; +}; + const isRepositoriesPath = ({ router }: RootState) => { const pathname = router.location ? router.location.pathname : ''; const match = matchRepositoriesRoute(pathname); @@ -35,7 +41,7 @@ const isSshKeysPath = ({ router }: RootState) => { }; export const MainContentBar = connect((state: RootState) => ({ - buttonVisible: !isWorkflowPath(state) && !isSshKeysPath(state) && !isRepositoriesPath(state) + buttonVisible: !isWorkflowPath(state) && !isSshKeysPath(state) && !isRepositoriesPath(state) && !isVirtualMachinePath(state) }), { onDetailsPanelToggle: toggleDetailsPanel })((props: MainContentBarProps) => diff --git a/src/views-components/project-properties-dialog/project-properties-form.tsx b/src/views-components/project-properties-dialog/project-properties-form.tsx index 82ae0406..90c8c080 100644 --- a/src/views-components/project-properties-dialog/project-properties-form.tsx +++ b/src/views-components/project-properties-dialog/project-properties-form.tsx @@ -2,94 +2,17 @@ // // SPDX-License-Identifier: AGPL-3.0 -import * as React from 'react'; -import { reduxForm, Field, reset } from 'redux-form'; -import { compose, Dispatch } from 'redux'; -import { ArvadosTheme } from '~/common/custom-theme'; -import { StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress } from '@material-ui/core'; -import { TagProperty } from '~/models/tag'; -import { TextField } from '~/components/text-field/text-field'; -import { TAG_VALUE_VALIDATION, TAG_KEY_VALIDATION } from '~/validators/validators'; +import { reduxForm, reset } from 'redux-form'; import { PROJECT_PROPERTIES_FORM_NAME, createProjectProperty } from '~/store/details-panel/details-panel-action'; +import { ResourcePropertiesForm, ResourcePropertiesFormData } from '~/views-components/resource-properties-form/resource-properties-form'; +import { withStyles } from '@material-ui/core'; -type CssRules = 'root' | 'keyField' | 'valueField' | 'buttonWrapper' | 'saveButton' | 'circularProgress'; +const Form = withStyles(({ spacing }) => ({ container: { marginBottom: spacing.unit * 2 } }))(ResourcePropertiesForm); -const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ - root: { - width: '100%', - display: 'flex' - }, - keyField: { - width: '40%', - marginRight: theme.spacing.unit * 3 - }, - valueField: { - width: '40%', - marginRight: theme.spacing.unit * 3 - }, - buttonWrapper: { - paddingTop: '14px', - position: 'relative', - }, - saveButton: { - boxShadow: 'none' - }, - circularProgress: { - position: 'absolute', - top: -9, - bottom: 0, - left: 0, - right: 0, - margin: 'auto' +export const ProjectPropertiesForm = reduxForm({ + form: PROJECT_PROPERTIES_FORM_NAME, + onSubmit: (data, dispatch) => { + dispatch(createProjectProperty(data)); + dispatch(reset(PROJECT_PROPERTIES_FORM_NAME)); } -}); - -interface ProjectPropertiesFormDataProps { - submitting: boolean; - invalid: boolean; - pristine: boolean; -} - -interface ProjectPropertiesFormActionProps { - handleSubmit: any; -} - -type ProjectPropertiesFormProps = ProjectPropertiesFormDataProps & ProjectPropertiesFormActionProps & WithStyles; - -export const ProjectPropertiesForm = compose( - reduxForm({ - form: PROJECT_PROPERTIES_FORM_NAME, - onSubmit: (data: TagProperty, dispatch: Dispatch) => { - dispatch(createProjectProperty(data)); - dispatch(reset(PROJECT_PROPERTIES_FORM_NAME)); - } - }), - withStyles(styles))( - ({ classes, submitting, pristine, invalid, handleSubmit }: ProjectPropertiesFormProps) => -
-
- -
-
- -
-
- - {submitting && } -
-
- ); +})(Form); diff --git a/src/views-components/resource-properties-form/property-field-common.tsx b/src/views-components/resource-properties-form/property-field-common.tsx new file mode 100644 index 00000000..028c46b9 --- /dev/null +++ b/src/views-components/resource-properties-form/property-field-common.tsx @@ -0,0 +1,45 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { connect } from 'react-redux'; +import { WrappedFieldMetaProps, WrappedFieldInputProps, WrappedFieldProps } from 'redux-form'; +import { identity } from 'lodash'; +import { Vocabulary } from '~/models/vocabulary'; +import { RootState } from '~/store/store'; +import { getVocabulary } from '~/store/vocabulary/vocabulary-selctors'; + +export interface VocabularyProp { + vocabulary: Vocabulary; +} + +export const mapStateToProps = (state: RootState): VocabularyProp => ({ + vocabulary: getVocabulary(state.properties), +}); + +export const connectVocabulary = connect(mapStateToProps); + +export const ITEMS_PLACEHOLDER: string[] = []; + +export const hasError = ({ touched, invalid }: WrappedFieldMetaProps) => + touched && invalid; + +export const getErrorMsg = (meta: WrappedFieldMetaProps) => + hasError(meta) + ? meta.error + : ''; + +export const handleBlur = ({ onBlur, value }: WrappedFieldInputProps) => + () => + onBlur(value); + +export const buildProps = ({ input, meta }: WrappedFieldProps) => ({ + value: input.value, + onChange: input.onChange, + onBlur: handleBlur(input), + items: ITEMS_PLACEHOLDER, + onSelect: input.onChange, + renderSuggestion: identity, + error: hasError(meta), + helperText: getErrorMsg(meta), +}); diff --git a/src/views-components/resource-properties-form/property-key-field.tsx b/src/views-components/resource-properties-form/property-key-field.tsx new file mode 100644 index 00000000..e6708a39 --- /dev/null +++ b/src/views-components/resource-properties-form/property-key-field.tsx @@ -0,0 +1,46 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from 'react'; +import { WrappedFieldProps, Field } from 'redux-form'; +import { memoize } from 'lodash'; +import { Autocomplete } from '~/components/autocomplete/autocomplete'; +import { Vocabulary } from '~/models/vocabulary'; +import { connectVocabulary, VocabularyProp, buildProps } from '~/views-components/resource-properties-form/property-field-common'; +import { TAG_KEY_VALIDATION } from '~/validators/validators'; + +export const PROPERTY_KEY_FIELD_NAME = 'key'; + +export const PropertyKeyField = connectVocabulary( + ({ vocabulary }: VocabularyProp) => + ); + +const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & VocabularyProp) => + ; + +const getValidation = memoize( + (vocabulary: Vocabulary) => + vocabulary.strict + ? [...TAG_KEY_VALIDATION, matchTags(vocabulary)] + : TAG_KEY_VALIDATION); + +const matchTags = (vocabulary: Vocabulary) => + (value: string) => + getTagsList(vocabulary).find(tag => tag.includes(value)) + ? undefined + : 'Incorrect key'; + +const getSuggestions = (value: string, vocabulary: Vocabulary) => + getTagsList(vocabulary).filter(tag => tag.includes(value) && tag !== value); + +const getTagsList = ({ tags }: Vocabulary) => + Object.keys(tags); diff --git a/src/views-components/resource-properties-form/property-value-field.tsx b/src/views-components/resource-properties-form/property-value-field.tsx new file mode 100644 index 00000000..db2db3f7 --- /dev/null +++ b/src/views-components/resource-properties-form/property-value-field.tsx @@ -0,0 +1,62 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from 'react'; +import { WrappedFieldProps, Field, formValues } from 'redux-form'; +import { compose } from 'redux'; +import { Autocomplete } from '~/components/autocomplete/autocomplete'; +import { Vocabulary } from '~/models/vocabulary'; +import { PROPERTY_KEY_FIELD_NAME } from '~/views-components/resource-properties-form/property-key-field'; +import { VocabularyProp, connectVocabulary, buildProps } from '~/views-components/resource-properties-form/property-field-common'; +import { TAG_VALUE_VALIDATION } from '~/validators/validators'; + +interface PropertyKeyProp { + propertyKey: string; +} + +type PropertyValueFieldProps = VocabularyProp & PropertyKeyProp; + +export const PROPERTY_VALUE_FIELD_NAME = 'value'; + +export const PropertyValueField = compose( + connectVocabulary, + formValues({ propertyKey: PROPERTY_KEY_FIELD_NAME }) +)( + (props: PropertyValueFieldProps) => + ); + +const PropertyValueInput = ({ vocabulary, propertyKey, ...props }: WrappedFieldProps & PropertyValueFieldProps) => + ; + +const getValidation = (props: PropertyValueFieldProps) => + isStrictTag(props.propertyKey, props.vocabulary) + ? [...TAG_VALUE_VALIDATION, matchTagValues(props)] + : TAG_VALUE_VALIDATION; + +const matchTagValues = ({ vocabulary, propertyKey }: PropertyValueFieldProps) => + (value: string) => + getTagValues(propertyKey, vocabulary).find(v => v.includes(value)) + ? undefined + : 'Incorrect value'; + +const getSuggestions = (value: string, tagName: string, vocabulary: Vocabulary) => + getTagValues(tagName, vocabulary).filter(v => v.includes(value) && v !== value); + +const isStrictTag = (tagName: string, vocabulary: Vocabulary) => { + const tag = vocabulary.tags[tagName]; + return tag ? tag.strict : false; +}; + +const getTagValues = (tagName: string, vocabulary: Vocabulary) => { + const tag = vocabulary.tags[tagName]; + return tag && tag.values ? tag.values : []; +}; diff --git a/src/views-components/resource-properties-form/resource-properties-form.tsx b/src/views-components/resource-properties-form/resource-properties-form.tsx new file mode 100644 index 00000000..a62b3d15 --- /dev/null +++ b/src/views-components/resource-properties-form/resource-properties-form.tsx @@ -0,0 +1,44 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from 'react'; +import { InjectedFormProps } from 'redux-form'; +import { Grid, withStyles, WithStyles } from '@material-ui/core'; +import { PropertyKeyField, PROPERTY_KEY_FIELD_NAME } from './property-key-field'; +import { PropertyValueField, PROPERTY_VALUE_FIELD_NAME } from './property-value-field'; +import { ProgressButton } from '~/components/progress-button/progress-button'; +import { GridClassKey } from '@material-ui/core/Grid'; + +export interface ResourcePropertiesFormData { + [PROPERTY_KEY_FIELD_NAME]: string; + [PROPERTY_VALUE_FIELD_NAME]: string; +} + +export type ResourcePropertiesFormProps = InjectedFormProps & WithStyles; + +export const ResourcePropertiesForm = ({ handleSubmit, submitting, invalid, classes }: ResourcePropertiesFormProps ) => +
+ + + + + + + + + + + +
; + +const Button = withStyles(theme => ({ + root: { marginTop: theme.spacing.unit } +}))(ProgressButton); diff --git a/src/views-components/side-panel-tree/side-panel-tree.tsx b/src/views-components/side-panel-tree/side-panel-tree.tsx index 33ee97f9..dd5005c3 100644 --- a/src/views-components/side-panel-tree/side-panel-tree.tsx +++ b/src/views-components/side-panel-tree/side-panel-tree.tsx @@ -10,7 +10,7 @@ import { TreeItem } from "~/components/tree/tree"; import { ProjectResource } from "~/models/project"; import { ListItemTextIcon } from "~/components/list-item-text-icon/list-item-text-icon"; import { ProjectIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, TrashIcon } from '~/components/icon/icon'; -import { RecentIcon, WorkflowIcon } from '~/components/icon/icon'; +import { WorkflowIcon } from '~/components/icon/icon'; import { activateSidePanelTreeItem, toggleSidePanelTreeItemCollapse, SIDE_PANEL_TREE, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions'; import { openSidePanelContextMenu } from '~/store/context-menu/context-menu-actions'; import { noop } from 'lodash'; @@ -59,8 +59,6 @@ const getSidePanelIcon = (category: string) => { return FavoriteIcon; case SidePanelTreeCategory.PROJECTS: return ProjectsIcon; - case SidePanelTreeCategory.RECENT_OPEN: - return RecentIcon; case SidePanelTreeCategory.SHARED_WITH_ME: return ShareMeIcon; case SidePanelTreeCategory.TRASH: diff --git a/src/views/collection-panel/collection-tag-form.tsx b/src/views/collection-panel/collection-tag-form.tsx index 9aa88128..fd4f0880 100644 --- a/src/views/collection-panel/collection-tag-form.tsx +++ b/src/views/collection-panel/collection-tag-form.tsx @@ -2,103 +2,17 @@ // // SPDX-License-Identifier: AGPL-3.0 -import * as React from 'react'; -import { reduxForm, Field, reset } from 'redux-form'; -import { compose, Dispatch } from 'redux'; -import { ArvadosTheme } from '~/common/custom-theme'; -import { StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress, Grid, Typography } from '@material-ui/core'; -import { TagProperty } from '~/models/tag'; -import { TextField } from '~/components/text-field/text-field'; +import { reduxForm, reset } from 'redux-form'; import { createCollectionTag, COLLECTION_TAG_FORM_NAME } from '~/store/collection-panel/collection-panel-action'; -import { TAG_VALUE_VALIDATION, TAG_KEY_VALIDATION } from '~/validators/validators'; +import { ResourcePropertiesForm, ResourcePropertiesFormData } from '~/views-components/resource-properties-form/resource-properties-form'; +import { withStyles } from '@material-ui/core'; -type CssRules = 'root' | 'keyField' | 'valueField' | 'buttonWrapper' | 'saveButton' | 'circularProgress'; +const Form = withStyles(({ spacing }) => ({ container: { marginBottom: spacing.unit * 2 } }))(ResourcePropertiesForm); -const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ - root: { - width: '100%', - display: 'flex' - }, - keyField: { - width: '25%', - marginRight: theme.spacing.unit * 3 - }, - valueField: { - width: '40%', - marginRight: theme.spacing.unit * 3 - }, - buttonWrapper: { - paddingTop: '14px', - position: 'relative', - }, - saveButton: { - boxShadow: 'none' - }, - circularProgress: { - position: 'absolute', - top: -9, - bottom: 0, - left: 0, - right: 0, - margin: 'auto' +export const CollectionTagForm = reduxForm({ + form: COLLECTION_TAG_FORM_NAME, + onSubmit: (data, dispatch) => { + dispatch(createCollectionTag(data)); + dispatch(reset(COLLECTION_TAG_FORM_NAME)); } -}); - -interface CollectionTagFormDataProps { - submitting: boolean; - invalid: boolean; - pristine: boolean; -} - -interface CollectionTagFormActionProps { - handleSubmit: any; -} - -type CollectionTagFormProps = CollectionTagFormDataProps & CollectionTagFormActionProps & WithStyles; - -export const CollectionTagForm = compose( - reduxForm({ - form: COLLECTION_TAG_FORM_NAME, - onSubmit: (data: TagProperty, dispatch: Dispatch) => { - dispatch(createCollectionTag(data)); - dispatch(reset(COLLECTION_TAG_FORM_NAME)); - } - }), - withStyles(styles))( - - class CollectionTagForm extends React.Component { - - render() { - const { classes, submitting, pristine, invalid, handleSubmit } = this.props; - return ( -
-
- -
-
- -
-
- - {submitting && } -
-
- ); - } - } - - ); +})(Form); diff --git a/src/views/favorite-panel/favorite-panel.tsx b/src/views/favorite-panel/favorite-panel.tsx index 33c901cb..4682d3fc 100644 --- a/src/views/favorite-panel/favorite-panel.tsx +++ b/src/views/favorite-panel/favorite-panel.tsx @@ -11,7 +11,6 @@ import { RouteComponentProps } from 'react-router'; import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters'; import { SortDirection } from '~/components/data-table/data-column'; import { ResourceKind } from '~/models/resource'; -import { resourceLabel } from '~/common/labels'; import { ArvadosTheme } from '~/common/custom-theme'; import { FAVORITE_PANEL_ID } from "~/store/favorite-panel/favorite-panel-action"; import { @@ -28,9 +27,11 @@ import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context- import { loadDetailsPanel } from '~/store/details-panel/details-panel-action'; import { navigateTo } from '~/store/navigation/navigation-action'; import { ContainerRequestState } from "~/models/container-request"; -import { FavoritesState } from '../../store/favorites/favorites-reducer'; +import { FavoritesState } from '~/store/favorites/favorites-reducer'; import { RootState } from '~/store/store'; import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view'; +import { createTree } from '~/models/tree'; +import { getSimpleObjectTypeFilters } from '~/store/resource-type-filters/resource-type-filters'; type CssRules = "toolbar" | "button"; @@ -57,57 +58,41 @@ export interface FavoritePanelFilter extends DataTableFilterItem { type: ResourceKind | ContainerRequestState; } -export const favoritePanelColumns: DataColumns = [ +export const favoritePanelColumns: DataColumns = [ { name: FavoritePanelColumnNames.NAME, selected: true, configurable: true, sortDirection: SortDirection.ASC, - filters: [], + filters: createTree(), render: uuid => }, { name: "Status", selected: true, configurable: true, - filters: [], + filters: createTree(), render: uuid => }, { name: FavoritePanelColumnNames.TYPE, selected: true, configurable: true, - filters: [ - { - name: resourceLabel(ResourceKind.COLLECTION), - selected: true, - type: ResourceKind.COLLECTION - }, - { - name: resourceLabel(ResourceKind.PROCESS), - selected: true, - type: ResourceKind.PROCESS - }, - { - name: resourceLabel(ResourceKind.PROJECT), - selected: true, - type: ResourceKind.PROJECT - } - ], + filters: getSimpleObjectTypeFilters(), render: uuid => }, { name: FavoritePanelColumnNames.OWNER, selected: true, configurable: true, - filters: [], + filters: createTree(), render: uuid => }, { name: FavoritePanelColumnNames.FILE_SIZE, selected: true, configurable: true, - filters: [], + filters: createTree(), render: uuid => }, { @@ -115,7 +100,7 @@ export const favoritePanelColumns: DataColumns = [ selected: true, configurable: true, sortDirection: SortDirection.NONE, - filters: [], + filters: createTree(), render: uuid => } ]; diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx index 1221d0d1..b8811476 100644 --- a/src/views/project-panel/project-panel.tsx +++ b/src/views/project-panel/project-panel.tsx @@ -3,16 +3,18 @@ // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; -import { DataExplorer } from "~/views-components/data-explorer/data-explorer"; +import withStyles from "@material-ui/core/styles/withStyles"; import { DispatchProp, connect } from 'react-redux'; -import { DataColumns } from '~/components/data-table/data-table'; import { RouteComponentProps } from 'react-router'; +import { StyleRulesCallback, WithStyles } from "@material-ui/core"; + +import { DataExplorer } from "~/views-components/data-explorer/data-explorer"; +import { DataColumns } from '~/components/data-table/data-table'; import { RootState } from '~/store/store'; import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters'; import { ContainerRequestState } from '~/models/container-request'; import { SortDirection } from '~/components/data-table/data-column'; import { ResourceKind, Resource } from '~/models/resource'; -import { resourceLabel } from '~/common/labels'; import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner } from '~/views-components/data-explorer/renderers'; import { ProjectIcon } from '~/components/icon/icon'; import { ResourceName } from '~/views-components/data-explorer/renderers'; @@ -24,9 +26,9 @@ import { navigateTo } from '~/store/navigation/navigation-action'; import { getProperty } from '~/store/properties/properties'; import { PROJECT_PANEL_CURRENT_UUID } from '~/store/project-panel/project-panel-action'; import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view'; -import { StyleRulesCallback, WithStyles } from "@material-ui/core"; import { ArvadosTheme } from "~/common/custom-theme"; -import withStyles from "@material-ui/core/styles/withStyles"; +import { createTree } from '~/models/tree'; +import { getInitialResourceTypeFilters } from '~/store/resource-type-filters/resource-type-filters'; type CssRules = 'root' | "button"; @@ -54,57 +56,41 @@ export interface ProjectPanelFilter extends DataTableFilterItem { type: ResourceKind | ContainerRequestState; } -export const projectPanelColumns: DataColumns = [ +export const projectPanelColumns: DataColumns = [ { name: ProjectPanelColumnNames.NAME, selected: true, configurable: true, sortDirection: SortDirection.ASC, - filters: [], + filters: createTree(), render: uuid => }, { name: "Status", selected: true, configurable: true, - filters: [], + filters: createTree(), render: uuid => , }, { name: ProjectPanelColumnNames.TYPE, selected: true, configurable: true, - filters: [ - { - name: resourceLabel(ResourceKind.COLLECTION), - selected: true, - type: ResourceKind.COLLECTION - }, - { - name: resourceLabel(ResourceKind.PROCESS), - selected: true, - type: ResourceKind.PROCESS - }, - { - name: resourceLabel(ResourceKind.PROJECT), - selected: true, - type: ResourceKind.PROJECT - } - ], + filters: getInitialResourceTypeFilters(), render: uuid => }, { name: ProjectPanelColumnNames.OWNER, selected: true, configurable: true, - filters: [], + filters: createTree(), render: uuid => }, { name: ProjectPanelColumnNames.FILE_SIZE, selected: true, configurable: true, - filters: [], + filters: createTree(), render: uuid => }, { @@ -112,13 +98,18 @@ export const projectPanelColumns: DataColumns = [ selected: true, configurable: true, sortDirection: SortDirection.NONE, - filters: [], + filters: createTree(), render: uuid => } ]; export const PROJECT_PANEL_ID = "projectPanel"; +const DEFAUL_VIEW_MESSAGES = [ + 'Your project is empty.', + 'Please create a project or create a collection and upload a data.', +]; + interface ProjectPanelDataProps { currentItemId: string; resources: ResourcesState; @@ -145,11 +136,8 @@ export const ProjectPanel = withStyles(styles)( dataTableDefaultView={ - }/> + messages={DEFAUL_VIEW_MESSAGES} /> + } />
; } diff --git a/src/views/run-process-panel/run-process-panel.tsx b/src/views/run-process-panel/run-process-panel.tsx index c8411ad7..c5b95c3b 100644 --- a/src/views/run-process-panel/run-process-panel.tsx +++ b/src/views/run-process-panel/run-process-panel.tsx @@ -6,7 +6,7 @@ import { Dispatch } from 'redux'; import { connect } from 'react-redux'; import { RootState } from '~/store/store'; import { RunProcessPanelRootDataProps, RunProcessPanelRootActionProps, RunProcessPanelRoot } from '~/views/run-process-panel/run-process-panel-root'; -import { goToStep, setWorkflow, runProcess, searchWorkflows, openSetWorkflowDialog } from '~/store/run-process-panel/run-process-panel-actions'; +import { goToStep, runProcess, searchWorkflows, openSetWorkflowDialog } from '~/store/run-process-panel/run-process-panel-actions'; import { WorkflowResource } from '~/models/workflow'; const mapStateToProps = ({ runProcessPanel }: RootState): RunProcessPanelRootDataProps => { diff --git a/src/views/search-results-panel/search-results-panel-view.tsx b/src/views/search-results-panel/search-results-panel-view.tsx index 009b2abe..ea658ee7 100644 --- a/src/views/search-results-panel/search-results-panel-view.tsx +++ b/src/views/search-results-panel/search-results-panel-view.tsx @@ -20,7 +20,9 @@ import { ResourceOwner, ResourceType } from '~/views-components/data-explorer/renderers'; - +import { createTree } from '~/models/tree'; +import { getInitialResourceTypeFilters } from '../../store/resource-type-filters/resource-type-filters'; +// TODO: code clean up export enum SearchResultsPanelColumnNames { NAME = "Name", PROJECT = "Project", @@ -48,64 +50,48 @@ export interface WorkflowPanelFilter extends DataTableFilterItem { type: ResourceKind | ContainerRequestState; } -export const searchResultsPanelColumns: DataColumns = [ +export const searchResultsPanelColumns: DataColumns = [ { name: SearchResultsPanelColumnNames.NAME, selected: true, configurable: true, sortDirection: SortDirection.ASC, - filters: [], + filters: createTree(), render: (uuid: string) => }, { name: SearchResultsPanelColumnNames.PROJECT, selected: true, configurable: true, - filters: [], + filters: createTree(), render: uuid => }, { name: SearchResultsPanelColumnNames.STATUS, selected: true, configurable: true, - filters: [], + filters: createTree(), render: uuid => }, { name: SearchResultsPanelColumnNames.TYPE, selected: true, configurable: true, - filters: [ - { - name: resourceLabel(ResourceKind.COLLECTION), - selected: true, - type: ResourceKind.COLLECTION - }, - { - name: resourceLabel(ResourceKind.PROCESS), - selected: true, - type: ResourceKind.PROCESS - }, - { - name: resourceLabel(ResourceKind.PROJECT), - selected: true, - type: ResourceKind.PROJECT - } - ], + filters: getInitialResourceTypeFilters(), render: (uuid: string) => , }, { name: SearchResultsPanelColumnNames.OWNER, selected: true, configurable: true, - filters: [], + filters: createTree(), render: uuid => }, { name: SearchResultsPanelColumnNames.FILE_SIZE, selected: true, configurable: true, - filters: [], + filters: createTree(), render: uuid => }, { @@ -113,7 +99,7 @@ export const searchResultsPanelColumns: DataColumns selected: true, configurable: true, sortDirection: SortDirection.NONE, - filters: [], + filters: createTree(), render: uuid => } ]; diff --git a/src/views/trash-panel/trash-panel.tsx b/src/views/trash-panel/trash-panel.tsx index a0cf3e4f..ae12425e 100644 --- a/src/views/trash-panel/trash-panel.tsx +++ b/src/views/trash-panel/trash-panel.tsx @@ -33,7 +33,9 @@ import { ContextMenuKind } from "~/views-components/context-menu/context-menu"; import { Dispatch } from "redux"; import { PanelDefaultView } from '~/components/panel-default-view/panel-default-view'; import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view'; - +import { createTree } from '~/models/tree'; +import { getInitialResourceTypeFilters } from '../../store/resource-type-filters/resource-type-filters'; +// TODO: code clean up type CssRules = "toolbar" | "button"; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ @@ -79,13 +81,13 @@ export const ResourceRestore = ); -export const trashPanelColumns: DataColumns = [ +export const trashPanelColumns: DataColumns = [ { name: TrashPanelColumnNames.NAME, selected: true, configurable: true, sortDirection: SortDirection.ASC, - filters: [], + filters: createTree(), render: uuid => }, { @@ -93,18 +95,7 @@ export const trashPanelColumns: DataColumns = [ selected: true, configurable: true, sortDirection: SortDirection.NONE, - filters: [ - { - name: resourceLabel(ResourceKind.COLLECTION), - selected: true, - type: ResourceKind.COLLECTION - }, - { - name: resourceLabel(ResourceKind.PROJECT), - selected: true, - type: ResourceKind.PROJECT - } - ], + filters: getInitialResourceTypeFilters(), render: uuid => , }, { @@ -112,7 +103,7 @@ export const trashPanelColumns: DataColumns = [ selected: true, configurable: true, sortDirection: SortDirection.NONE, - filters: [], + filters: createTree(), render: uuid => }, { @@ -120,7 +111,7 @@ export const trashPanelColumns: DataColumns = [ selected: true, configurable: true, sortDirection: SortDirection.NONE, - filters: [], + filters: createTree(), render: uuid => }, { @@ -128,7 +119,7 @@ export const trashPanelColumns: DataColumns = [ selected: true, configurable: true, sortDirection: SortDirection.NONE, - filters: [], + filters: createTree(), render: uuid => }, { @@ -136,7 +127,7 @@ export const trashPanelColumns: DataColumns = [ selected: true, configurable: false, sortDirection: SortDirection.NONE, - filters: [], + filters: createTree(), render: uuid => } ]; diff --git a/src/views/virtual-machine-panel/virtual-machine-panel.tsx b/src/views/virtual-machine-panel/virtual-machine-panel.tsx new file mode 100644 index 00000000..c94c3a74 --- /dev/null +++ b/src/views/virtual-machine-panel/virtual-machine-panel.tsx @@ -0,0 +1,200 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Grid, Typography, Button, Card, CardContent, TableBody, TableCell, TableHead, TableRow, Table, Tooltip } from '@material-ui/core'; +import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles'; +import { ArvadosTheme } from '~/common/custom-theme'; +import { DefaultCodeSnippet } from '~/components/default-code-snippet/default-code-snippet'; +import { Link } from 'react-router-dom'; +import { Dispatch, compose } from 'redux'; +import { saveRequestedDate, loadVirtualMachinesData } from '~/store/virtual-machines/virtual-machines-actions'; +import { RootState } from '~/store/store'; +import { ListResults } from '~/services/common-service/common-resource-service'; +import { HelpIcon } from '~/components/icon/icon'; +import { VirtualMachinesLoginsResource, VirtualMachinesResource } from '~/models/virtual-machines'; +import { Routes } from '~/routes/routes'; + +type CssRules = 'button' | 'codeSnippet' | 'link' | 'linkIcon' | 'rightAlign' | 'cardWithoutMachines' | 'icon'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + button: { + marginTop: theme.spacing.unit, + marginBottom: theme.spacing.unit + }, + codeSnippet: { + borderRadius: theme.spacing.unit * 0.5, + border: '1px solid', + borderColor: theme.palette.grey["400"], + }, + link: { + textDecoration: 'none', + color: theme.palette.primary.main, + "&:hover": { + color: theme.palette.primary.dark, + transition: 'all 0.5s ease' + } + }, + linkIcon: { + textDecoration: 'none', + color: theme.palette.grey["500"], + textAlign: 'right', + "&:hover": { + color: theme.palette.common.black, + transition: 'all 0.5s ease' + } + }, + rightAlign: { + textAlign: "right" + }, + cardWithoutMachines: { + display: 'flex' + }, + icon: { + textAlign: "right", + marginTop: theme.spacing.unit + } +}); + +const mapStateToProps = ({ virtualMachines }: RootState) => { + return { + requestedDate: virtualMachines.date, + ...virtualMachines + }; +}; + +const mapDispatchToProps = { + saveRequestedDate, + loadVirtualMachinesData +}; + +interface VirtualMachinesPanelDataProps { + requestedDate: string; + virtualMachines: ListResults; + logins: VirtualMachinesLoginsResource[]; + links: ListResults; +} + +interface VirtualMachinesPanelActionProps { + saveRequestedDate: () => void; + loadVirtualMachinesData: () => string; +} + +type VirtualMachineProps = VirtualMachinesPanelActionProps & VirtualMachinesPanelDataProps & WithStyles; + +export const VirtualMachinePanel = compose( + withStyles(styles), + connect(mapStateToProps, mapDispatchToProps))( + class extends React.Component { + componentDidMount() { + this.props.loadVirtualMachinesData(); + } + + render() { + const { virtualMachines, links } = this.props; + return ( + + {virtualMachines.itemsAvailable === 0 && } + {virtualMachines.itemsAvailable > 0 && links.itemsAvailable > 0 && } + {} + + ); + } + } + ); + +const CardContentWithNoVirtualMachines = (props: VirtualMachineProps) => + + + + + + You do not have access to any virtual machines. Some Arvados features require using the command line. You may request access to a hosted virtual machine with the command line shell. + + + + + {props.requestedDate && + + A request for shell access was sent on {props.requestedDate} + } + + + + ; + +const CardContentWithVirtualMachines = (props: VirtualMachineProps) => + + + +
+ + {props.requestedDate && + + A request for shell access was sent on {props.requestedDate} + } +
+ + + + + Host name + Login name + Command line + Web shell + + + + {props.virtualMachines.items.map((it, index) => + + {it.hostname} + {getUsername(props.links, it)} + ssh {getUsername(props.links, it)}@shell.arvados + + + Log in as {getUsername(props.links, it)} + + + + )} + +
+
+
+
; + +const getUsername = (links: ListResults, virtualMachine: VirtualMachinesResource) => { + const link = links.items.find((item: any) => item.headUuid === virtualMachine.uuid); + return link.properties.username || undefined; +}; + +const CardSSHSection = (props: VirtualMachineProps) => + + + + + In order to access virtual machines using SSH, add an SSH key to your account and add a section like this to your SSH configuration file ( ~/.ssh/config): + + + + + ; + +const textSSH = `Host *.arvados + TCPKeepAlive yes + ServerAliveInterval 60 + ProxyCommand ssh -p2222 turnout@switchyard.api.ardev.roche.com -x -a $SSH_PROXY_FLAGS %h`; \ No newline at end of file diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index 8542e6ca..2cff4317 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -49,6 +49,7 @@ import { MyAccountPanel } from '~/views/my-account-panel/my-account-panel'; import { SharingDialog } from '~/views-components/sharing-dialog/sharing-dialog'; import { AdvancedTabDialog } from '~/views-components/advanced-tab-dialog/advanced-tab-dialog'; import { ProcessInputDialog } from '~/views-components/process-input-dialog/process-input-dialog'; +import { VirtualMachinePanel } from '~/views/virtual-machine-panel/virtual-machine-panel'; import { ProjectPropertiesDialog } from '~/views-components/project-properties-dialog/project-properties-dialog'; import { RepositoriesPanel } from '~/views/repositories-panel/repositories-panel'; import { RepositoriesSampleGitDialog } from '~/views-components/repositories-sample-git-dialog/repositories-sample-git-dialog'; @@ -128,6 +129,7 @@ export const WorkbenchPanel = + diff --git a/src/views/workflow-panel/workflow-panel-view.tsx b/src/views/workflow-panel/workflow-panel-view.tsx index 18a254bd..da8a0c4b 100644 --- a/src/views/workflow-panel/workflow-panel-view.tsx +++ b/src/views/workflow-panel/workflow-panel-view.tsx @@ -11,14 +11,16 @@ import { ResourceLastModifiedDate, RosurceWorkflowName, ResourceWorkflowStatus, - ResourceShare + ResourceShare, + ResourceRunProcess } from "~/views-components/data-explorer/renderers"; import { SortDirection } from '~/components/data-table/data-column'; import { DataColumns } from '~/components/data-table/data-table'; import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters'; import { Grid, Paper } from '@material-ui/core'; import { WorkflowDetailsCard } from './workflow-description-card'; -import { WorkflowResource } from '../../models/workflow'; +import { WorkflowResource } from '~/models/workflow'; +import { createTree } from '~/models/tree'; export enum WorkflowPanelColumnNames { NAME = "Name", @@ -61,36 +63,38 @@ const resourceStatus = (type: string) => { } }; -export const workflowPanelColumns: DataColumns = [ +export const workflowPanelColumns: DataColumns = [ { name: WorkflowPanelColumnNames.NAME, selected: true, configurable: true, sortDirection: SortDirection.ASC, - filters: [], + filters: createTree(), render: (uuid: string) => }, { name: WorkflowPanelColumnNames.AUTHORISATION, selected: true, configurable: true, - filters: [ - { - name: resourceStatus(ResourceStatus.PUBLIC), - selected: true, - type: ResourceStatus.PUBLIC - }, - { - name: resourceStatus(ResourceStatus.PRIVATE), - selected: true, - type: ResourceStatus.PRIVATE - }, - { - name: resourceStatus(ResourceStatus.SHARED), - selected: true, - type: ResourceStatus.SHARED - } - ], + filters: createTree(), + // TODO: restore filters + // filters: [ + // { + // name: resourceStatus(ResourceStatus.PUBLIC), + // selected: true, + // type: ResourceStatus.PUBLIC + // }, + // { + // name: resourceStatus(ResourceStatus.PRIVATE), + // selected: true, + // type: ResourceStatus.PRIVATE + // }, + // { + // name: resourceStatus(ResourceStatus.SHARED), + // selected: true, + // type: ResourceStatus.SHARED + // } + // ], render: (uuid: string) => , }, { @@ -98,20 +102,27 @@ export const workflowPanelColumns: DataColumns = [ selected: true, configurable: true, sortDirection: SortDirection.NONE, - filters: [], + filters: createTree(), render: (uuid: string) => }, { name: '', selected: true, configurable: false, - filters: [], + filters: createTree(), render: (uuid: string) => + }, + { + name: '', + selected: true, + configurable: false, + filters: createTree(), + render: (uuid: string) => } ]; export const WorkflowPanelView = (props: WorkflowPanelProps) => { - return + return ({ }); const mapDispatchToProps = (dispatch: Dispatch): WorfklowPanelActionProps => ({ - handleRowDoubleClick: (uuid: string) => { dispatch(navigateTo(uuid)); },