Currently this configuration schema is supported:
```
{
- "API_HOST": "string"
+ "API_HOST": "string",
+ "VOCABULARY_URL": "string"
}
```
"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/",
version: string;
websocketUrl: string;
workbenchUrl: string;
+ vocabularyUrl: string;
}
export const fetchConfig = () => {
.catch(() => Promise.resolve(getDefaultConfig()))
.then(config => Axios
.get<Config>(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,
+ })));
};
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`;
// 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';
value: string;
items: Item[];
suggestions?: Suggestion[];
+ error?: boolean;
+ helperText?: string;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
render() {
return (
<RootRef rootRef={this.containerRef}>
- <FormControl fullWidth>
+ <FormControl fullWidth error={this.props.error}>
{this.renderLabel()}
{this.renderInput()}
+ {this.renderHelperText()}
{this.renderSuggestions()}
</FormControl>
</RootRef>
/>;
}
+ renderHelperText(){
+ return <FormHelperText>{this.props.helperText}</FormHelperText>;
+ }
+
renderSuggestions() {
const { suggestions = [] } = this.props;
return (
<Popper
open={this.state.suggestionsOpen && suggestions.length > 0}
- anchorEl={this.containerRef.current}>
+ anchorEl={this.inputRef.current}>
<Paper onMouseDown={this.preventBlur}>
<List dense style={{ width: this.getSuggestionsWidth() }}>
{suggestions.map(
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';
onColumnToggle: (column: DataColumn<T>) => void;
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
onSortToggle: (column: DataColumn<T>) => void;
- onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<T>) => void;
+ onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => void;
onChangePage: (page: number) => void;
onChangeRowsPerPage: (rowsPerPage: number) => void;
extractKey?: (item: T) => React.Key;
selected: true,
configurable: false,
sortDirection: SortDirection.NONE,
- filters: [],
+ filters: createTree(),
key: "context-actions",
render: this.renderContextMenuTrigger
};
--- /dev/null
+// 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<CssRules> = (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<DataTableFilterProps & WithStyles<CssRules>, DataTableFilterState> {
+ state: DataTableFilterState = {
+ anchorEl: undefined,
+ filters: createTree(),
+ prevFilters: createTree(),
+ };
+ icon = React.createRef<HTMLElement>();
+
+ render() {
+ const { name, classes, children } = this.props;
+ const isActive = getNodeDescendants('')(this.state.filters).some(f => f.selected);
+ return <>
+ <Tooltip title='Filters'>
+ <ButtonBase
+ className={classnames([classes.root, { [classes.active]: isActive }])}
+ component="span"
+ onClick={this.open}
+ disableRipple>
+ {children}
+ <i className={classnames(["fas fa-filter", classes.icon])}
+ data-fa-transform="shrink-3"
+ ref={this.icon} />
+ </ButtonBase>
+ </Tooltip>
+ <Popover
+ anchorEl={this.state.anchorEl}
+ open={!!this.state.anchorEl}
+ anchorOrigin={DefaultTransformOrigin}
+ transformOrigin={DefaultTransformOrigin}
+ onClose={this.cancel}>
+ <Card>
+ <CardContent>
+ <Typography variant="caption">
+ {name}
+ </Typography>
+ </CardContent>
+ <DataTableFiltersTree
+ filters={this.state.filters}
+ onChange={filters => this.setState({ filters })} />
+ <CardActions>
+ <Button
+ color="primary"
+ variant="raised"
+ size="small"
+ onClick={this.submit}>
+ Ok
+ </Button>
+ <Button
+ color="primary"
+ variant="outlined"
+ size="small"
+ onClick={this.cancel}>
+ Cancel
+ </Button>
+ </CardActions >
+ </Card>
+ </Popover>
+ </>;
+ }
+
+ 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 });
+ }
+
+ }
+);
--- /dev/null
+// 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<DataTableFilterItem>;
+
+export interface DataTableFilterProps {
+ filters: DataTableFilters;
+ onChange?: (filters: DataTableFilters) => void;
+}
+
+export class DataTableFiltersTree extends React.Component<DataTableFilterProps> {
+
+ render() {
+ const { filters } = this.props;
+ const hasSubfilters = countNodes(filters) !== countChildren('')(filters);
+ return <TreeComponent
+ levelIndentation={hasSubfilters ? 20 : 0}
+ itemRightPadding={20}
+ items={filtersToTree(filters)}
+ render={renderItem}
+ showSelection
+ disableRipple
+ onContextMenu={noop}
+ toggleItemActive={noop}
+ toggleItemOpen={this.toggleOpen}
+ toggleItemSelection={this.toggleFilter}
+ />;
+ }
+
+ toggleFilter = (_: React.MouseEvent, item: TreeItem<DataTableFilterItem>) => {
+ const { onChange = noop } = this.props;
+ onChange(toggleNodeSelection(item.id)(this.props.filters));
+ }
+
+ toggleOpen = (_: React.MouseEvent, item: TreeItem<DataTableFilterItem>) => {
+ const { onChange = noop } = this.props;
+ onChange(toggleNodeCollapse(item.id)(this.props.filters));
+ }
+}
+
+const renderItem = (item: TreeItem<DataTableFilterItem>) =>
+ <span>{item.data.name}</span>;
+
+const filterToTreeItem = (filters: DataTableFilters) =>
+ (id: string): TreeItem<any> => {
+ const node = getNode(id)(filters) || initTreeNode({ id: '', value: 'InvalidNode' });
+ const items = getNodeChildrenIds(node.id)(filters)
+ .map(filterToTreeItem(filters));
+
+ 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<DataTableFilterItem>[] =>
+ map(filterToTreeItem(filters), getNodeChildrenIds('')(filters));
} 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";
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<DataTableFilterProps & WithStyles<CssRules>, DataTableFilterState> {
state: DataTableFilterState = {
anchorEl: undefined,
filters: [],
- prevFilters: []
+ prevFilters: [],
+ filtersTree: filters,
};
icon = React.createRef<HTMLElement>();
</ListItem>
)}
</List>
+ <DataTableFiltersTree
+ filters={this.state.filtersTree}
+ onChange={filtersTree => this.setState({ filtersTree })} />
<CardActions>
<Button
color="primary"
// SPDX-License-Identifier: AGPL-3.0
import * as React from "react";
-import { DataTableFilterItem } from "../data-table-filters/data-table-filters";
+import { DataTableFilters } from "../data-table-filters/data-table-filters-tree";
+import { createTree } from '~/models/tree';
-export interface DataColumn<T, F extends DataTableFilterItem = DataTableFilterItem> {
+export interface DataColumn<T> {
key?: React.Key;
name: string;
selected: boolean;
configurable: boolean;
sortDirection?: SortDirection;
- filters: F[];
+ filters: DataTableFilters;
render: (item: T) => React.ReactElement<any>;
renderHeader?: () => React.ReactElement<any>;
}
return column.sortDirection ? { ...column, sortDirection: SortDirection.NONE } : column;
};
-export const createDataColumn = <T, F extends DataTableFilterItem>(dataColumn: Partial<DataColumn<T, F>>): DataColumn<T, F> => ({
+export const createDataColumn = <T>(dataColumn: Partial<DataColumn<T>>): DataColumn<T> => ({
key: '',
name: '',
selected: true,
configurable: true,
sortDirection: SortDirection.NONE,
- filters: [],
+ filters: createTree(),
render: () => React.createElement('span'),
...dataColumn,
});
import * as React from "react";
import { mount, configure } from "enzyme";
+import { pipe } from 'lodash/fp';
import { TableHead, TableCell, Typography, TableBody, Button, TableSortLabel } from "@material-ui/core";
import * as Adapter from "enzyme-adapter-react-16";
import { DataTable, DataColumns } from "./data-table";
-import { DataTableFilters } from "../data-table-filters/data-table-filters";
+import { DataTableFilters } from "~/components/data-table-filters/data-table-filters";
import { SortDirection, createDataColumn } from "./data-column";
-import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+import { DataTableFiltersPopover } from '~/components/data-table-filters/data-table-filters-popover';
+import { createTree, setNode, initTreeNode } from '~/models/tree';
+import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters-tree";
configure({ adapter: new Adapter() });
it("passes sorting props to <TableSortLabel />", () => {
const columns: DataColumns<string> = [
createDataColumn({
- name: "Column 1",
- sortDirection: SortDirection.ASC,
- selected: true,
- configurable: true,
- render: (item) => <Typography>{item}</Typography>
- })];
+ name: "Column 1",
+ sortDirection: SortDirection.ASC,
+ selected: true,
+ configurable: true,
+ render: (item) => <Typography>{item}</Typography>
+ })];
const onSortToggle = jest.fn();
const dataTable = mount(<DataTable
columns={columns}
expect(dataTable.find(DataTableFilters)).toHaveLength(0);
});
- it("passes filter props to <DataTableFilter />", () => {
+ it("passes filter props to <DataTableFiltersPopover />", () => {
+ const filters = pipe(
+ () => createTree<DataTableFilterItem>(),
+ setNode(initTreeNode({ id: 'filter', value: { name: 'filter' } }))
+ );
const columns: DataColumns<string> = [{
name: "Column 1",
sortDirection: SortDirection.ASC,
selected: true,
configurable: true,
- filters: [{ name: "Filter 1", selected: true }],
+ filters: filters(),
render: (item) => <Typography>{item}</Typography>
}];
const onFiltersChange = jest.fn();
onRowDoubleClick={jest.fn()}
onSortToggle={jest.fn()}
onContextMenu={jest.fn()} />);
- expect(dataTable.find(DataTableFilters).prop("filters")).toBe(columns[0].filters);
- dataTable.find(DataTableFilters).prop("onChange")([]);
+ expect(dataTable.find(DataTableFiltersPopover).prop("filters")).toBe(columns[0].filters);
+ dataTable.find(DataTableFiltersPopover).prop("onChange")([]);
expect(onFiltersChange).toHaveBeenCalledWith([], columns[0]);
});
import * as React from 'react';
import { Table, TableBody, TableRow, TableCell, TableHead, TableSortLabel, StyleRulesCallback, Theme, WithStyles, withStyles } from '@material-ui/core';
import { DataColumn, SortDirection } from './data-column';
-import { DataTableFilters, DataTableFilterItem } from "../data-table-filters/data-table-filters";
import { DataTableDefaultView } from '../data-table-default-view/data-table-default-view';
+import { DataTableFilters } from '../data-table-filters/data-table-filters-tree';
+import { DataTableFiltersPopover } from '../data-table-filters/data-table-filters-popover';
+import { countNodes } from '~/models/tree';
-export type DataColumns<T, F extends DataTableFilterItem = DataTableFilterItem> = Array<DataColumn<T, F>>;
+export type DataColumns<T> = Array<DataColumn<T>>;
export interface DataTableDataProps<T> {
items: T[];
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
onRowDoubleClick: (event: React.MouseEvent<HTMLTableRowElement>, item: T) => void;
onSortToggle: (column: DataColumn<T>) => void;
- onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<T>) => void;
+ onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => void;
extractKey?: (item: T) => React.Key;
working?: boolean;
defaultView?: React.ReactNode;
return <TableCell key={key || index}>
{renderHeader ?
renderHeader() :
- filters.length > 0
- ? <DataTableFilters
+ countNodes(filters) > 0
+ ? <DataTableFiltersPopover
name={`${name} filters`}
onChange={filters =>
onFiltersChange &&
onFiltersChange(filters, column)}
filters={filters}>
{name}
- </DataTableFilters>
+ </DataTableFiltersPopover>
: sortDirection
? <TableSortLabel
active={sortDirection !== SortDirection.NONE}
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import AccessTime from '@material-ui/icons/AccessTime';
import Add from '@material-ui/icons/Add';
import ArrowBack from '@material-ui/icons/ArrowBack';
import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
export const ProjectIcon: IconType = (props) => <Folder {...props} />;
export const ProjectsIcon: IconType = (props) => <Inbox {...props} />;
export const ProvenanceGraphIcon: IconType = (props) => <DeviceHub {...props} />;
-export const RecentIcon: IconType = (props) => <AccessTime {...props} />;
export const RemoveIcon: IconType = (props) => <Delete {...props} />;
export const RemoveFavoriteIcon: IconType = (props) => <Star {...props} />;
export const RenameIcon: IconType = (props) => <Edit {...props} />;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import Button, { ButtonProps } from '@material-ui/core/Button';
+import { CircularProgress, withStyles } from '@material-ui/core';
+import { CircularProgressProps } from '@material-ui/core/CircularProgress';
+
+interface ProgressButtonProps extends ButtonProps {
+ loading?: boolean;
+ progressProps?: CircularProgressProps;
+}
+
+export const ProgressButton = ({ loading, progressProps, children, disabled, ...props }: ProgressButtonProps) =>
+ <Button {...props} disabled={disabled || loading}>
+ {children}
+ {loading && <Progress {...progressProps} size={getProgressSize(props.size)} />}
+ </Button>;
+
+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;
+ }
+};
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
render: (item: TreeItem<T>, level?: number) => ReactElement<{}>;
showSelection?: boolean | ((item: TreeItem<T>) => boolean);
+ levelIndentation?: number;
+ itemRightPadding?: number;
toggleItemActive: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
toggleItemOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
toggleItemSelection?: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
? this.props.showSelection
: () => this.props.showSelection ? true : false;
+ const { levelIndentation = 20, itemRightPadding = 20 } = this.props;
+
return <List component="div" className={list}>
{items && items.map((it: TreeItem<T>, idx: number) =>
<div key={`item/${level}/${idx}`}>
- <ListItem button className={listItem} style={{ paddingLeft: (level + 1) * 20 }}
+ <ListItem button className={listItem}
+ style={{
+ paddingLeft: (level + 1) * levelIndentation,
+ paddingRight: itemRightPadding,
+ }}
disableRipple={disableRipple}
onClick={event => toggleItemActive(event, it)}
onContextMenu={this.handleRowContextMenu(it)}>
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()}]`);
store.dispatch(setBuildInfo());
store.dispatch(setCurrentTokenDialogApiHost(apiHost));
store.dispatch(setUuidPrefix(config.uuidPrefix));
+ store.dispatch(loadVocabulary);
const TokenComponent = (props: any) => <ApiToken authService={services.authService} {...props} />;
const MainPanelComponent = (props: any) => <MainPanel {...props} />;
});
};
+// force build comment #1
export const getCollectionUrl = (uuid: string) => {
return `/collections/${uuid}`;
};
+
+export enum CollectionType {
+ GENERAL = 'nil',
+ OUTPUT = 'output',
+ LOG = 'log',
+}
// 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 {
REPOSITORY = "arvados#repository",
SSH_KEY = "arvados#authorizedKeys",
USER = "arvados#user",
+ VIRTUAL_MACHINE = "arvados#virtualMachine",
WORKFLOW = "arvados#workflow",
NONE = "arvados#none"
}
LOG = '57u5n',
REPOSITORY = 's0uqq',
USER = 'tpzed',
+ VIRTUAL_MACHINE = '2x53u',
WORKFLOW = '7fd4e',
+ SSH_KEY = 'fngyi'
}
export const RESOURCE_UUID_PATTERN = '.{5}-.{5}-.{15}';
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;
}
export const getNodeDescendants = (id: string, limit = Infinity) => <T>(tree: Tree<T>) =>
mapIdsToNodes(getNodeDescendantsIds(id, limit)(tree))(tree);
+export const countNodes = <T>(tree: Tree<T>) =>
+ getNodeDescendantsIds('')(tree).length;
+
+export const countChildren = (id: string) => <T>(tree: Tree<T>) =>
+ getNodeChildren('')(tree).length;
+
export const getNodeDescendantsIds = (id: string, limit = Infinity) => <T>(tree: Tree<T>): string[] => {
const node = getNode(id)(tree);
const children = node ? node.children :
ids.map(id => getNode(id)(tree)).filter((node): node is TreeNode<T> => node !== undefined);
export const activateNode = (id: string) => <T>(tree: Tree<T>) =>
- mapTree(node => node.id === id ? { ...node, active: true } : { ...node, active: false })(tree);
+ mapTree((node: TreeNode<T>) => node.id === id ? { ...node, active: true } : { ...node, active: false })(tree);
export const deactivateNode = <T>(tree: Tree<T>) =>
- mapTree(node => node.active ? { ...node, active: false } : node)(tree);
+ mapTree((node: TreeNode<T>) => node.active ? { ...node, active: false } : node)(tree);
export const expandNode = (...ids: string[]) => <T>(tree: Tree<T>) =>
- mapTree(node => ids.some(id => id === node.id) ? { ...node, expanded: true } : node)(tree);
+ mapTree((node: TreeNode<T>) => ids.some(id => id === node.id) ? { ...node, expanded: true } : node)(tree);
export const collapseNode = (...ids: string[]) => <T>(tree: Tree<T>) =>
- mapTree(node => ids.some(id => id === node.id) ? { ...node, expanded: false } : node)(tree);
+ mapTree((node: TreeNode<T>) => ids.some(id => id === node.id) ? { ...node, expanded: false } : node)(tree);
export const toggleNodeCollapse = (...ids: string[]) => <T>(tree: Tree<T>) =>
- mapTree(node => ids.some(id => id === node.id) ? { ...node, expanded: !node.expanded } : node)(tree);
+ mapTree((node: TreeNode<T>) => ids.some(id => id === node.id) ? { ...node, expanded: !node.expanded } : node)(tree);
export const setNodeStatus = (id: string) => (status: TreeNodeStatus) => <T>(tree: Tree<T>) => {
const node = getNode(id)(tree);
return ids.reduce((tree, id) => deselectNode(id)(tree), tree);
};
+export const getSelectedNodes = <T>(tree: Tree<T>) =>
+ getNodeDescendants('')(tree)
+ .filter(node => node.selected);
+
export const initTreeNode = <T>(data: Pick<TreeNode<T>, 'id' | 'value'> & { parent?: string }): TreeNode<T> => ({
children: [],
active: false,
ownerUuid: string;
identityUrl: string;
prefs: userPrefs;
+ isAdmin: boolean;
}
export const getUserFullname = (user?: User) => {
--- /dev/null
+// 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
--- /dev/null
+// 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<string, Tag>;
+}
+
+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
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';
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);
store.dispatch(loadWorkflow);
} else if (searchResultsMatch) {
store.dispatch(loadSearchResults);
+ } else if (virtualMachineMatch) {
+ store.dispatch(loadVirtualMachines);
} else if(repositoryMatch) {
store.dispatch(loadRepositories);
} else if (sshKeysMatch) {
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`,
export const matchSearchResultsRoute = (route: string) =>
matchPath<ResourceRouteParams>(route, { path: Routes.SEARCH_RESULTS });
+export const matchVirtualMachineRoute = (route: string) =>
+ matchPath<ResourceRouteParams>(route, { path: Routes.VIRTUAL_MACHINES });
+
export const matchRepositoriesRoute = (route: string) =>
matchPath<ResourceRouteParams>(route, { path: Routes.REPOSITORIES });
).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
expect(new FilterBuilder()
.addIn("etag", ["etagValue1", "etagValue2"], "myPrefix")
.getFilters())
- .toEqual(`["my_prefix.etag","in",["etagValue1","etagValue2"]]`);
+ .toEqual(`["myPrefix.etag","in",["etagValue1","etagValue2"]]`);
});
});
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);
}
}
const resPrefix = resourcePrefix
- ? _.snakeCase(resourcePrefix) + "."
+ ? resourcePrefix + "."
: "";
this.filters += `${this.filters ? "," : ""}["${resPrefix}${_.snakeCase(field)}","${cond}",${value}]`;
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';
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;
}
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));
}
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);
}
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
};
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<typeof createServices>;
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);
const favoriteService = new FavoriteService(linkService, groupsService);
const tagService = new TagService(linkService);
const searchService = new SearchService();
+ const vocabularyService = new VocabularyService(config.vocabularyUrl);
return {
ancestorsService,
searchService,
tagService,
userService,
+ virtualMachineService,
webdavClient,
workflowService,
+ vocabularyService,
};
};
--- /dev/null
+// 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<VirtualMachinesResource> {
+ 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
--- /dev/null
+// 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<Vocabulary>(this.url)
+ .then(response => response.data);
+ }
+}
//
// 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';
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;
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<any>, getState: () => RootState, services: ServiceRepository) => {
- const { resources } = getState();
const kind = extractUuidKind(uuid);
- const data = getResource<any>(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<any>(getDataForAdvancedTab(uuid));
+ const advanceDataCollection: AdvancedTabDialogData = advancedTabData(uuid, metaCollection, userCollection, collectionApiResponse, dataCollection, CollectionData.COLLECTION, GroupContentsResourcePrefix.COLLECTION, CollectionData.STORAGE_CLASSES_CONFIRMED, dataCollection.storageClassesConfirmed);
+ dispatch<any>(initAdvancedTabDialog(advanceDataCollection));
+ break;
+ case ResourceKind.PROCESS:
+ const { data: dataProcess, metadata: metaProcess, user: userProcess } = await dispatch<any>(getDataForAdvancedTab(uuid));
+ const advancedDataProcess: AdvancedTabDialogData = advancedTabData(uuid, metaProcess, userProcess, containerRequestApiResponse, dataProcess, ProcessData.CONTAINER_REQUEST, GroupContentsResourcePrefix.PROCESS, ProcessData.OUTPUT_NAME, dataProcess.outputName);
+ dispatch<any>(initAdvancedTabDialog(advancedDataProcess));
+ break;
+ case ResourceKind.PROJECT:
+ const { data: dataProject, metadata: metaProject, user: userProject } = await dispatch<any>(getDataForAdvancedTab(uuid));
+ const advanceDataProject: AdvancedTabDialogData = advancedTabData(uuid, metaProject, userProject, groupRequestApiResponse, dataProject, ProjectData.GROUP, GroupContentsResourcePrefix.PROJECT, ProjectData.DELETE_AT, dataProject.deleteAt);
+ dispatch<any>(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<any>(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<any>(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<any>, getState: () => RootState, services: ServiceRepository) => {
+ const { resources } = getState();
+ const data = getResource<any>(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,
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;
};
const cliGetExample = (uuid: string, resourceKind: string) => {
const cliGetExample = `arv ${resourceKind} get \\
- --uuid ${uuid}`;
+ --uuid ${uuid}`;
return cliGetExample;
};
`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;
};
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} \\
- <<EOF
+ -H "Authorization: OAuth2 $ARVADOS_API_TOKEN" \\
+ --data-urlencode ${resourceKind}@/dev/stdin \\
+ https://$ARVADOS_API_HOST/arvados/v1/${resourcePrefix}/${uuid} \\
+ <<EOF
{
"${resourceName}": ${resource}
}
"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
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';
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
}
});
});
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: []
});
});
uuid: "uuid",
ownerUuid: "ownerUuid",
identityUrl: "identityUrl",
- prefs: {}
+ prefs: {},
+ isAdmin: false
};
const state = reducer(initialState, authActions.USER_DETAILS_SUCCESS(user));
lastName: "Doe",
uuid: "uuid",
ownerUuid: "ownerUuid",
+ isAdmin: false
}
});
});
(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<any>(openContextMenu(event, resource));
// 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';
// 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<any> }>(),
- 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 }>(),
dataExplorerActions.REQUEST_ITEMS({ id }),
SET_COLUMNS: (payload: { columns: DataColumns<any> }) =>
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 }),
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;
return this.id;
}
- public getColumnFilters<T, F extends DataTableFilterItem>(columns: DataColumns<T, F>, columnName: string): F[] {
- const column = columns.find(c => c.name === columnName);
- return column ? column.filters.filter(f => f.selected) : [];
+ public getColumnFilters<T>(columns: DataColumns<T>, columnName: string): DataTableFilters {
+ return getDataExplorerColumnFilters(columns, columnName);
}
abstract requestItems(api: MiddlewareAPI<Dispatch, RootState>): void;
}
-export const getDataExplorerColumnFilters = <T, F extends DataTableFilterItem>(columns: DataColumns<T, F>, columnName: string): F[] => {
+export const getDataExplorerColumnFilters = <T>(columns: DataColumns<T>, 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 = <R>(dataExplorer: DataExplorer) => ({
+export const dataExplorerToListParams = (dataExplorer: DataExplorer) => ({
limit: dataExplorer.rowsPerPage,
offset: dataExplorer.page * dataExplorer.rowsPerPage,
});
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", () => {
selected: true,
configurable: false,
sortDirection: SortDirection.NONE,
- filters: [],
+ filters: createTree<DataTableFilterItem>(),
render: jest.fn()
}],
requestItems: jest.fn(),
selected: true,
configurable: false,
sortDirection: SortDirection.NONE,
- filters: [],
+ filters: createTree<DataTableFilterItem>(),
render: jest.fn()
}],
requestItems: jest.fn(),
};
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);
});
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<any>;
? { ...column, selected: !column.selected }
: column;
-const setFilters = (columnName: string, filters: DataTableFilterItem[]) =>
+const setFilters = (columnName: string, filters: DataTableFilters) =>
(column: DataColumn<any>) => column.name === columnName
? { ...column, filters }
: column;
//
// 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";
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) {
if (!dataExplorer) {
api.dispatch(favoritesPanelDataExplorerIsNotSet());
} else {
- const columns = dataExplorer.columns as DataColumns<string, FavoritePanelFilter>;
+ const columns = dataExplorer.columns as DataColumns<string>;
const sortColumn = getSortColumn(dataExplorer);
- const typeFilters = this.getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
+ const typeFilters = serializeSimpleObjectTypeFilters(getDataExplorerColumnFilters(columns, FavoritePanelColumnNames.TYPE));
+
const linkOrder = new OrderBuilder<LinkResource>();
const contentOrder = new OrderBuilder<GroupContentsResource>();
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));
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);
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';
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const process = getProcess(resource.uuid)(getState().resources);
if (process) {
- const processStatus = getProcessStatus(process);
- if (processStatus === ProcessStatus.DRAFT) {
- dispatch<any>(resetPickerProjectTree());
- dispatch<any>(initProjectsTreePicker(PROCESS_COPY_FORM_NAME));
- const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, uuid: resource.uuid, ownerUuid: '' };
- dispatch<any>(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<any>(resetPickerProjectTree());
+ dispatch<any>(initProjectsTreePicker(PROCESS_COPY_FORM_NAME));
+ const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, uuid: resource.uuid, ownerUuid: '' };
+ dispatch<any>(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 }));
}
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) {
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';
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const process = getProcess(resource.uuid)(getState().resources);
if (process) {
- const processStatus = getProcessStatus(process);
- if (processStatus === ProcessStatus.DRAFT) {
- dispatch<any>(resetPickerProjectTree());
- dispatch<any>(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<any>(resetPickerProjectTree());
+ dispatch<any>(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 }));
}
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;
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 }));
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;
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 }));
//
// 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";
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());
};
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';
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) {
});
export const getFilters = (dataExplorer: DataExplorer) => {
- const columns = dataExplorer.columns as DataColumns<string, ProjectPanelFilter>;
- 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<string>;
+ 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) => {
--- /dev/null
+// 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"]]`);
+ });
+});
--- /dev/null
+// 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<DataTableFilterItem>({
+ id: name,
+ value: { name },
+ parent,
+ children: [],
+ active: false,
+ selected: true,
+ expanded: false,
+ status: TreeNodeStatus.LOADED,
+ });
+
+export const getSimpleObjectTypeFilters = pipe(
+ (): DataTableFilters => createTree<DataTableFilterItem>(),
+ initFilter(ObjectTypeFilter.PROJECT),
+ initFilter(ObjectTypeFilter.PROCESS),
+ initFilter(ObjectTypeFilter.COLLECTION),
+);
+
+export const getInitialResourceTypeFilters = pipe(
+ (): DataTableFilters => createTree<DataTableFilterItem>(),
+ 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<DataTableFilterItem>[]) =>
+ 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<typeof createFiltersBuilder>) => {
+ 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<typeof createFiltersBuilder>) => 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<DataTableFilterItem>) => {
+ return getSelectedNodes(filters)
+ .map(f => f.id)
+ .map(objectTypeToResourceKind);
+};
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
const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
const dialog = getDialog<string>(getState().dialog, SHARING_DIALOG_NAME);
-
if (dialog) {
dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
- const { items } = await permissionService.listResourcePermissions(dialog.data);
- dispatch<any>(initializePublicAccessForm(items));
- await dispatch<any>(initializeManagementForm(items));
- dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
+ try {
+ const { items } = await permissionService.listResourcePermissions(dialog.data);
+ dispatch<any>(initializePublicAccessForm(items));
+ await dispatch<any>(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));
+ }
}
};
PROJECTS = 'Projects',
SHARED_WITH_ME = 'Shared with me',
WORKFLOWS = 'Workflows',
- RECENT_OPEN = 'Recently open',
FAVORITES = 'Favorites',
TRASH = 'Trash'
}
const SIDE_PANEL_CATEGORIES = [
SidePanelTreeCategory.WORKFLOWS,
- SidePanelTreeCategory.RECENT_OPEN,
SidePanelTreeCategory.FAVORITES,
SidePanelTreeCategory.TRASH,
];
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 =
runProcessPanel: runProcessPanelReducer,
appInfo: appInfoReducer,
searchBar: searchBarReducer,
+ virtualMachines: virtualMachinesReducer,
repositories: repositoriesReducer
});
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) {
async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
const dataExplorer = api.getState().dataExplorer[this.getId()];
- const columns = dataExplorer.columns as DataColumns<string, TrashPanelFilter>;
+ const columns = dataExplorer.columns as DataColumns<string>;
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<ProjectResource>();
.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
});
--- /dev/null
+// 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<string>(),
+ SET_VIRTUAL_MACHINES: ofType<ListResults<any>>(),
+ SET_LOGINS: ofType<VirtualMachinesLoginsResource[]>(),
+ SET_LINKS: ofType<ListResults<any>>()
+});
+
+export type VirtualMachineActions = UnionOf<typeof virtualMachinesActions>;
+
+export const VIRTUAL_MACHINES_PANEL = 'virtualMachinesPanel';
+
+export const openVirtualMachines = () =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch<any>(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<any>(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<any>(loadRequestedDate());
+ };
+
+const virtualMachinesBindedActions = bindDataExplorerActions(VIRTUAL_MACHINES_PANEL);
+
+export const loadVirtualMachinesPanel = () =>
+ (dispatch: Dispatch) => {
+ dispatch(virtualMachinesBindedActions.REQUEST_ITEMS());
+ };
--- /dev/null
+// 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<any>;
+ logins: VirtualMachinesLoginsResource[];
+ links: ListResults<any>;
+}
+
+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
+ });
--- /dev/null
+// 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,
+ }));
+};
--- /dev/null
+// 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>(VOCABULARY_PROPERTY_NAME)(state) || DEFAULT_VOCABULARY;
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';
await dispatch(loadSearchResultsPanel());
});
+export const loadVirtualMachines = handleFirstTimeLoad(
+ async (dispatch: Dispatch<any>) => {
+ await dispatch(loadVirtualMachinesPanel());
+ dispatch(setBreadcrumbs([{ label: 'Virtual Machines' }]));
+ });
+
export const loadRepositories = handleFirstTimeLoad(
async (dispatch: Dispatch<any>) => {
await dispatch(loadRepositoriesPanel());
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';
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) =>
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<any>(navigateToRunProcess);
+ dispatch(runProcessPanelActions.RESET_RUN_PROCESS_PANEL());
+ dispatch<any>(goToStep(1));
+ dispatch(runProcessPanelActions.SET_STEP_CHANGED(true));
+ dispatch(runProcessPanelActions.SET_SELECTED_WORKFLOW(workflow!));
+ };
+
export const getPublicUserUuid = (state: RootState) => {
const prefix = getProperty<string>(UUID_PREFIX_PROPERTY_NAME)(state.properties);
return `${prefix}-tpzed-anonymouspublic`;
<TableCell className={props.classes.cell}>{it.uuid}</TableCell>
<TableCell className={props.classes.cell}>{it.linkClass}</TableCell>
<TableCell className={props.classes.cell}>{it.name}</TableCell>
- <TableCell className={props.classes.cell}>{props.user ? `User: ${props.user.firstName} ${props.user.lastName}` : it.tailUuid}</TableCell>
+ <TableCell className={props.classes.cell}>{props.user && `User: ${props.user.firstName} ${props.user.lastName}`}</TableCell>
<TableCell className={props.classes.cell}>{it.headUuid === props.uuid ? 'this' : it.headUuid}</TableCell>
<TableCell className={props.classes.cell}>{JSON.stringify(it.properties)}</TableCell>
</TableRow>
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";
dispatch<any>(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",
}, {
name: "Advanced",
icon: AdvancedIcon,
- execute: (dispatch, { uuid, index }) => {
- dispatch<any>(openAdvancedTabDialog(uuid, index));
+ execute: (dispatch, resource) => {
+ dispatch<any>(openAdvancedTabDialog(resource.uuid, resource.index));
}
}, {
name: "Remove",
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",
name: "Advanced",
icon: AdvancedIcon,
execute: (dispatch, { uuid, index }) => {
- // ToDo
+ dispatch<any>(openAdvancedTabDialog(uuid, index));
}
}, {
name: "Remove",
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';
// 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';
export const CurrentTokenDialog =
withStyles(styles)(
connect(getCurrentTokenDialogData)(
- withDialog('currentTokenDialog')(
+ withDialog(CURRENT_TOKEN_DIALOG_NAME)(
class extends React.Component<CurrentTokenProps> {
render() {
const { classes, open, closeDialog, ...data } = this.props;
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;
dispatch(dataExplorerActions.TOGGLE_SORT({ id, columnName: column.name }));
},
- onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<any>) => {
+ onFiltersChange: (filters: DataTableFilters, column: DataColumn<any>) => {
dispatch(dataExplorerActions.SET_FILTERS({ id, columnName: column.name, filters }));
},
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';
return `${uuidPrefix}-tpzed-anonymouspublic`;
};
-// ToDo: share onClick
export const resourceShare = (dispatch: Dispatch, uuidPrefix: string, ownerUuid?: string, uuid?: string) => {
const isPublic = ownerUuid === getPublicUuid(uuidPrefix);
return (
<div>
- { isPublic && uuid &&
+ {!isPublic && uuid &&
<Tooltip title="Share">
<IconButton onClick={() => dispatch<any>(openSharingDialog(uuid))}>
<ShareIcon />
})((props: { ownerUuid?: string, uuidPrefix: string, uuid?: string } & DispatchProp<any>) =>
resourceShare(props.dispatch, props.uuidPrefix, props.ownerUuid, props.uuid));
+export const resourceRunProcess = (dispatch: Dispatch, uuid: string) => {
+ return (
+ <div>
+ {uuid &&
+ <Tooltip title="Run process">
+ <IconButton onClick={() => dispatch<any>(openRunProcess(uuid))}>
+ <ProcessIcon />
+ </IconButton>
+ </Tooltip>}
+ </div>
+ );
+};
+
+export const ResourceRunProcess = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
+ return {
+ uuid: resource ? resource.uuid : ''
+ };
+ })((props: { uuid: string } & DispatchProp<any>) =>
+ resourceRunProcess(props.dispatch, props.uuid));
+
export const renderWorkflowStatus = (uuidPrefix: string, ownerUuid?: string) => {
if (ownerUuid === getPublicUuid(uuidPrefix)) {
return renderStatus(ResourceStatus.PUBLIC);
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;
<MenuItem>
{getUserFullname(user)}
</MenuItem>
+ <MenuItem onClick={() => dispatch(openVirtualMachines())}>Virtual Machines</MenuItem>
<MenuItem onClick={() => dispatch(openRepositoriesPanel())}>Repositories</MenuItem>
<MenuItem onClick={() => dispatch(openCurrentTokenDialog)}>Current token</MenuItem>
<MenuItem onClick={() => dispatch(navigateToSshKeys)}>Ssh Keys</MenuItem>
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 {
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);
};
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) =>
//
// 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<CssRules> = (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<ResourcePropertiesFormData>({
+ form: PROJECT_PROPERTIES_FORM_NAME,
+ onSubmit: (data, dispatch) => {
+ dispatch<any>(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<CssRules>;
-
-export const ProjectPropertiesForm = compose(
- reduxForm({
- form: PROJECT_PROPERTIES_FORM_NAME,
- onSubmit: (data: TagProperty, dispatch: Dispatch) => {
- dispatch<any>(createProjectProperty(data));
- dispatch(reset(PROJECT_PROPERTIES_FORM_NAME));
- }
- }),
- withStyles(styles))(
- ({ classes, submitting, pristine, invalid, handleSubmit }: ProjectPropertiesFormProps) =>
- <form onSubmit={handleSubmit} className={classes.root}>
- <div className={classes.keyField}>
- <Field name="key"
- disabled={submitting}
- component={TextField}
- validate={TAG_KEY_VALIDATION}
- label="Key" />
- </div>
- <div className={classes.valueField}>
- <Field name="value"
- disabled={submitting}
- component={TextField}
- validate={TAG_VALUE_VALIDATION}
- label="Value" />
- </div>
- <div className={classes.buttonWrapper}>
- <Button type="submit" className={classes.saveButton}
- color="primary"
- size='small'
- disabled={invalid || submitting || pristine}
- variant="contained">
- ADD
- </Button>
- {submitting && <CircularProgress size={20} className={classes.circularProgress} />}
- </div>
- </form>
- );
+})(Form);
--- /dev/null
+// 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),
+});
--- /dev/null
+// 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) =>
+ <Field
+ name={PROPERTY_KEY_FIELD_NAME}
+ component={PropertyKeyInput}
+ vocabulary={vocabulary}
+ validate={getValidation(vocabulary)} />);
+
+const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & VocabularyProp) =>
+ <Autocomplete
+ label='Key'
+ suggestions={getSuggestions(props.input.value, vocabulary)}
+ {...buildProps(props)}
+ />;
+
+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);
--- /dev/null
+// 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) =>
+ <Field
+ name={PROPERTY_VALUE_FIELD_NAME}
+ component={PropertyValueInput}
+ validate={getValidation(props)}
+ {...props} />);
+
+const PropertyValueInput = ({ vocabulary, propertyKey, ...props }: WrappedFieldProps & PropertyValueFieldProps) =>
+ <Autocomplete
+ label='Value'
+ suggestions={getSuggestions(props.input.value, propertyKey, vocabulary)}
+ {...buildProps(props)}
+ />;
+
+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 : [];
+};
--- /dev/null
+// 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<ResourcePropertiesFormData> & WithStyles<GridClassKey>;
+
+export const ResourcePropertiesForm = ({ handleSubmit, submitting, invalid, classes }: ResourcePropertiesFormProps ) =>
+ <form onSubmit={handleSubmit}>
+ <Grid container spacing={16} classes={classes}>
+ <Grid item xs>
+ <PropertyKeyField />
+ </Grid>
+ <Grid item xs>
+ <PropertyValueField />
+ </Grid>
+ <Grid item xs>
+ <Button
+ disabled={invalid}
+ loading={submitting}
+ color='primary'
+ variant='contained'
+ type='submit'>
+ Add
+ </Button>
+ </Grid>
+ </Grid>
+ </form>;
+
+const Button = withStyles(theme => ({
+ root: { marginTop: theme.spacing.unit }
+}))(ProgressButton);
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';
return FavoriteIcon;
case SidePanelTreeCategory.PROJECTS:
return ProjectsIcon;
- case SidePanelTreeCategory.RECENT_OPEN:
- return RecentIcon;
case SidePanelTreeCategory.SHARED_WITH_ME:
return ShareMeIcon;
case SidePanelTreeCategory.TRASH:
//
// 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<CssRules> = (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<ResourcePropertiesFormData>({
+ form: COLLECTION_TAG_FORM_NAME,
+ onSubmit: (data, dispatch) => {
+ dispatch<any>(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<CssRules>;
-
-export const CollectionTagForm = compose(
- reduxForm({
- form: COLLECTION_TAG_FORM_NAME,
- onSubmit: (data: TagProperty, dispatch: Dispatch) => {
- dispatch<any>(createCollectionTag(data));
- dispatch(reset(COLLECTION_TAG_FORM_NAME));
- }
- }),
- withStyles(styles))(
-
- class CollectionTagForm extends React.Component<CollectionTagFormProps> {
-
- render() {
- const { classes, submitting, pristine, invalid, handleSubmit } = this.props;
- return (
- <form onSubmit={handleSubmit} className={classes.root}>
- <div className={classes.keyField}>
- <Field name="key"
- disabled={submitting}
- component={TextField}
- validate={TAG_KEY_VALIDATION}
- label="Key" />
- </div>
- <div className={classes.valueField}>
- <Field name="value"
- disabled={submitting}
- component={TextField}
- validate={TAG_VALUE_VALIDATION}
- label="Value" />
- </div>
- <div className={classes.buttonWrapper}>
- <Button type="submit" className={classes.saveButton}
- color="primary"
- size='small'
- disabled={invalid || submitting || pristine}
- variant="contained">
- ADD
- </Button>
- {submitting && <CircularProgress size={20} className={classes.circularProgress} />}
- </div>
- </form>
- );
- }
- }
-
- );
+})(Form);
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 {
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";
type: ResourceKind | ContainerRequestState;
}
-export const favoritePanelColumns: DataColumns<string, FavoritePanelFilter> = [
+export const favoritePanelColumns: DataColumns<string> = [
{
name: FavoritePanelColumnNames.NAME,
selected: true,
configurable: true,
sortDirection: SortDirection.ASC,
- filters: [],
+ filters: createTree(),
render: uuid => <ResourceName uuid={uuid} />
},
{
name: "Status",
selected: true,
configurable: true,
- filters: [],
+ filters: createTree(),
render: uuid => <ProcessStatus uuid={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 => <ResourceType uuid={uuid} />
},
{
name: FavoritePanelColumnNames.OWNER,
selected: true,
configurable: true,
- filters: [],
+ filters: createTree(),
render: uuid => <ResourceOwner uuid={uuid} />
},
{
name: FavoritePanelColumnNames.FILE_SIZE,
selected: true,
configurable: true,
- filters: [],
+ filters: createTree(),
render: uuid => <ResourceFileSize uuid={uuid} />
},
{
selected: true,
configurable: true,
sortDirection: SortDirection.NONE,
- filters: [],
+ filters: createTree(),
render: uuid => <ResourceLastModifiedDate uuid={uuid} />
}
];
// 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';
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";
type: ResourceKind | ContainerRequestState;
}
-export const projectPanelColumns: DataColumns<string, ProjectPanelFilter> = [
+export const projectPanelColumns: DataColumns<string> = [
{
name: ProjectPanelColumnNames.NAME,
selected: true,
configurable: true,
sortDirection: SortDirection.ASC,
- filters: [],
+ filters: createTree(),
render: uuid => <ResourceName uuid={uuid} />
},
{
name: "Status",
selected: true,
configurable: true,
- filters: [],
+ filters: createTree(),
render: uuid => <ProcessStatus uuid={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 => <ResourceType uuid={uuid} />
},
{
name: ProjectPanelColumnNames.OWNER,
selected: true,
configurable: true,
- filters: [],
+ filters: createTree(),
render: uuid => <ResourceOwner uuid={uuid} />
},
{
name: ProjectPanelColumnNames.FILE_SIZE,
selected: true,
configurable: true,
- filters: [],
+ filters: createTree(),
render: uuid => <ResourceFileSize uuid={uuid} />
},
{
selected: true,
configurable: true,
sortDirection: SortDirection.NONE,
- filters: [],
+ filters: createTree(),
render: uuid => <ResourceLastModifiedDate uuid={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;
dataTableDefaultView={
<DataTableDefaultView
icon={ProjectIcon}
- messages={[
- 'Your project is empty.',
- 'Please create a project or create a collection and upload a data.'
- ]}/>
- }/>
+ messages={DEFAUL_VIEW_MESSAGES} />
+ } />
</div>;
}
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 => {
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",
type: ResourceKind | ContainerRequestState;
}
-export const searchResultsPanelColumns: DataColumns<string, WorkflowPanelFilter> = [
+export const searchResultsPanelColumns: DataColumns<string> = [
{
name: SearchResultsPanelColumnNames.NAME,
selected: true,
configurable: true,
sortDirection: SortDirection.ASC,
- filters: [],
+ filters: createTree(),
render: (uuid: string) => <ResourceName uuid={uuid} />
},
{
name: SearchResultsPanelColumnNames.PROJECT,
selected: true,
configurable: true,
- filters: [],
+ filters: createTree(),
render: uuid => <ResourceFileSize uuid={uuid} />
},
{
name: SearchResultsPanelColumnNames.STATUS,
selected: true,
configurable: true,
- filters: [],
+ filters: createTree(),
render: uuid => <ProcessStatus uuid={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) => <ResourceType uuid={uuid} />,
},
{
name: SearchResultsPanelColumnNames.OWNER,
selected: true,
configurable: true,
- filters: [],
+ filters: createTree(),
render: uuid => <ResourceOwner uuid={uuid} />
},
{
name: SearchResultsPanelColumnNames.FILE_SIZE,
selected: true,
configurable: true,
- filters: [],
+ filters: createTree(),
render: uuid => <ResourceFileSize uuid={uuid} />
},
{
selected: true,
configurable: true,
sortDirection: SortDirection.NONE,
- filters: [],
+ filters: createTree(),
render: uuid => <ResourceLastModifiedDate uuid={uuid} />
}
];
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<CssRules> = (theme: ArvadosTheme) => ({
</Tooltip>
);
-export const trashPanelColumns: DataColumns<string, TrashPanelFilter> = [
+export const trashPanelColumns: DataColumns<string> = [
{
name: TrashPanelColumnNames.NAME,
selected: true,
configurable: true,
sortDirection: SortDirection.ASC,
- filters: [],
+ filters: createTree(),
render: uuid => <ResourceName uuid={uuid} />
},
{
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 => <ResourceType uuid={uuid} />,
},
{
selected: true,
configurable: true,
sortDirection: SortDirection.NONE,
- filters: [],
+ filters: createTree(),
render: uuid => <ResourceFileSize uuid={uuid} />
},
{
selected: true,
configurable: true,
sortDirection: SortDirection.NONE,
- filters: [],
+ filters: createTree(),
render: uuid => <ResourceTrashDate uuid={uuid} />
},
{
selected: true,
configurable: true,
sortDirection: SortDirection.NONE,
- filters: [],
+ filters: createTree(),
render: uuid => <ResourceDeleteDate uuid={uuid} />
},
{
selected: true,
configurable: false,
sortDirection: SortDirection.NONE,
- filters: [],
+ filters: createTree(),
render: uuid => <ResourceRestore uuid={uuid} />
}
];
--- /dev/null
+// 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<CssRules> = (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<any>;
+ logins: VirtualMachinesLoginsResource[];
+ links: ListResults<any>;
+}
+
+interface VirtualMachinesPanelActionProps {
+ saveRequestedDate: () => void;
+ loadVirtualMachinesData: () => string;
+}
+
+type VirtualMachineProps = VirtualMachinesPanelActionProps & VirtualMachinesPanelDataProps & WithStyles<CssRules>;
+
+export const VirtualMachinePanel = compose(
+ withStyles(styles),
+ connect(mapStateToProps, mapDispatchToProps))(
+ class extends React.Component<VirtualMachineProps> {
+ componentDidMount() {
+ this.props.loadVirtualMachinesData();
+ }
+
+ render() {
+ const { virtualMachines, links } = this.props;
+ return (
+ <Grid container spacing={16}>
+ {virtualMachines.itemsAvailable === 0 && <CardContentWithNoVirtualMachines {...this.props} />}
+ {virtualMachines.itemsAvailable > 0 && links.itemsAvailable > 0 && <CardContentWithVirtualMachines {...this.props} />}
+ {<CardSSHSection {...this.props} />}
+ </Grid>
+ );
+ }
+ }
+ );
+
+const CardContentWithNoVirtualMachines = (props: VirtualMachineProps) =>
+ <Grid item xs={12}>
+ <Card>
+ <CardContent className={props.classes.cardWithoutMachines}>
+ <Grid item xs={6}>
+ <Typography variant="body2">
+ You do not have access to any virtual machines. Some Arvados features require using the command line. You may request access to a hosted virtual machine with the command line shell.
+ </Typography>
+ </Grid>
+ <Grid item xs={6} className={props.classes.rightAlign}>
+ <Button variant="contained" color="primary" className={props.classes.button} onClick={props.saveRequestedDate}>
+ SEND REQUEST FOR SHELL ACCESS
+ </Button>
+ {props.requestedDate &&
+ <Typography variant="body1">
+ A request for shell access was sent on {props.requestedDate}
+ </Typography>}
+ </Grid>
+ </CardContent>
+ </Card>
+ </Grid>;
+
+const CardContentWithVirtualMachines = (props: VirtualMachineProps) =>
+ <Grid item xs={12}>
+ <Card>
+ <CardContent>
+ <div className={props.classes.rightAlign}>
+ <Button variant="contained" color="primary" className={props.classes.button} onClick={props.saveRequestedDate}>
+ SEND REQUEST FOR SHELL ACCESS
+ </Button>
+ {props.requestedDate &&
+ <Typography variant="body1">
+ A request for shell access was sent on {props.requestedDate}
+ </Typography>}
+ </div>
+ <div className={props.classes.icon}>
+ <a href="https://doc.arvados.org/user/getting_started/vm-login-with-webshell.html" target="_blank" className={props.classes.linkIcon}>
+ <Tooltip title="Access VM using webshell">
+ <HelpIcon />
+ </Tooltip>
+ </a>
+ </div>
+ <Table>
+ <TableHead>
+ <TableRow>
+ <TableCell>Host name</TableCell>
+ <TableCell>Login name</TableCell>
+ <TableCell>Command line</TableCell>
+ <TableCell>Web shell</TableCell>
+ </TableRow>
+ </TableHead>
+ <TableBody>
+ {props.virtualMachines.items.map((it, index) =>
+ <TableRow key={index}>
+ <TableCell>{it.hostname}</TableCell>
+ <TableCell>{getUsername(props.links, it)}</TableCell>
+ <TableCell>ssh {getUsername(props.links, it)}@shell.arvados</TableCell>
+ <TableCell>
+ <a href={`https://workbench.c97qk.arvadosapi.com${it.href}/webshell/${getUsername(props.links, it)}`} target="_blank" className={props.classes.link}>
+ Log in as {getUsername(props.links, it)}
+ </a>
+ </TableCell>
+ </TableRow>
+ )}
+ </TableBody>
+ </Table>
+ </CardContent>
+ </Card>
+ </Grid>;
+
+const getUsername = (links: ListResults<any>, virtualMachine: VirtualMachinesResource) => {
+ const link = links.items.find((item: any) => item.headUuid === virtualMachine.uuid);
+ return link.properties.username || undefined;
+};
+
+const CardSSHSection = (props: VirtualMachineProps) =>
+ <Grid item xs={12}>
+ <Card>
+ <CardContent>
+ <Typography variant="body2">
+ In order to access virtual machines using SSH, <Link to={Routes.SSH_KEYS} className={props.classes.link}>add an SSH key to your account</Link> and add a section like this to your SSH configuration file ( ~/.ssh/config):
+ </Typography>
+ <DefaultCodeSnippet
+ className={props.classes.codeSnippet}
+ lines={[textSSH]} />
+ </CardContent>
+ </Card>
+ </Grid>;
+
+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
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';
<Route path={Routes.RUN_PROCESS} component={RunProcessPanel} />
<Route path={Routes.WORKFLOWS} component={WorkflowPanel} />
<Route path={Routes.SEARCH_RESULTS} component={SearchResultsPanel} />
+ <Route path={Routes.VIRTUAL_MACHINES} component={VirtualMachinePanel} />
<Route path={Routes.REPOSITORIES} component={RepositoriesPanel} />
<Route path={Routes.SSH_KEYS} component={SshKeyPanel} />
<Route path={Routes.MY_ACCOUNT} component={MyAccountPanel} />
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",
}
};
-export const workflowPanelColumns: DataColumns<string, WorkflowPanelFilter> = [
+export const workflowPanelColumns: DataColumns<string> = [
{
name: WorkflowPanelColumnNames.NAME,
selected: true,
configurable: true,
sortDirection: SortDirection.ASC,
- filters: [],
+ filters: createTree(),
render: (uuid: string) => <RosurceWorkflowName uuid={uuid} />
},
{
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) => <ResourceWorkflowStatus uuid={uuid} />,
},
{
selected: true,
configurable: true,
sortDirection: SortDirection.NONE,
- filters: [],
+ filters: createTree(),
render: (uuid: string) => <ResourceLastModifiedDate uuid={uuid} />
},
{
name: '',
selected: true,
configurable: false,
- filters: [],
+ filters: createTree(),
render: (uuid: string) => <ResourceShare uuid={uuid} />
+ },
+ {
+ name: '',
+ selected: true,
+ configurable: false,
+ filters: createTree(),
+ render: (uuid: string) => <ResourceRunProcess uuid={uuid} />
}
];
export const WorkflowPanelView = (props: WorkflowPanelProps) => {
- return <Grid container spacing={16} style={{minHeight: '500px'}}>
+ return <Grid container spacing={16} style={{ minHeight: '500px' }}>
<Grid item xs={6}>
<DataExplorer
id={WORKFLOW_PANEL_ID}
});
const mapDispatchToProps = (dispatch: Dispatch): WorfklowPanelActionProps => ({
-
handleRowDoubleClick: (uuid: string) => {
dispatch<any>(navigateTo(uuid));
},