"react-router-dom": "4.3.1",
"react-router-redux": "5.0.0-alpha.9",
"react-scripts-ts": "2.17.0",
+ "react-splitter-layout": "3.0.1",
+ "react-transition-group": "2.4.0",
"redux": "4.0.0",
"redux-thunk": "2.3.0",
- "unionize": "2.1.2"
+ "unionize": "2.1.2",
+ "uuid": "3.3.2"
},
"scripts": {
"start": "react-scripts-ts start",
"@types/react-router-redux": "5.0.15",
"@types/redux-devtools": "3.0.44",
"@types/redux-form": "7.4.5",
+ "@types/uuid": "3.4.4",
"axios-mock-adapter": "1.15.0",
"enzyme": "3.4.4",
"enzyme-adapter-react-16": "1.2.0",
.get<ConfigJSON>(CONFIG_URL + "?nocache=" + (new Date()).getTime())
.then(response => response.data)
.catch(() => Promise.resolve(getDefaultConfig()))
- .then(config => Axios.get<Config>(getDiscoveryURL(config.API_HOST)))
- .then(response => response.data);
+ .then(config => Axios
+ .get<Config>(getDiscoveryURL(config.API_HOST))
+ .then(response => ({ config: response.data, apiHost: config.API_HOST })));
+
};
export const mockConfig = (config: Partial<Config>): Config => ({
import green from '@material-ui/core/colors/green';
import yellow from '@material-ui/core/colors/yellow';
import red from '@material-ui/core/colors/red';
+import teal from '@material-ui/core/colors/teal';
export interface ArvadosThemeOptions extends ThemeOptions {
customs: any;
yellow700: string;
red900: string;
blue500: string;
- grey500: string;
- grey700: string;
}
-const red900 = red["900"];
+const arvadosPurple = '#361336';
const purple800 = purple["800"];
-const grey200 = grey["200"];
-const grey300 = grey["300"];
const grey500 = grey["500"];
const grey600 = grey["600"];
const grey700 = grey["700"];
yellow700: yellow["700"],
red900: red['900'],
blue500: blue['500'],
- grey500,
- grey700
}
},
overrides: {
},
MuiAppBar: {
colorPrimary: {
- backgroundColor: purple800
+ backgroundColor: arvadosPurple
}
},
MuiTabs: {
color: grey600
},
indicator: {
- backgroundColor: purple800
+ backgroundColor: arvadosPurple
}
},
MuiTab: {
selected: {
fontWeight: 700,
- color: purple800
+ color: arvadosPurple
}
},
MuiList: {
},
underline: {
'&:after': {
- borderBottomColor: purple800
+ borderBottomColor: arvadosPurple
},
'&:hover:not($disabled):not($focused):not($error):before': {
borderBottom: '1px solid inherit'
},
focused: {
"&$focused:not($error)": {
- color: purple800
+ color: arvadosPurple
}
}
}
},
palette: {
primary: {
- main: rocheBlue,
- dark: blue.A100
+ main: teal.A700,
+ dark: blue.A100,
+ contrastText: '#fff'
}
}
};
import * as React from 'react';
import { StyleRulesCallback, WithStyles, Typography, withStyles, Theme } from '@material-ui/core';
import { ArvadosTheme } from '~/common/custom-theme';
+import * as classNames from 'classnames';
type CssRules = 'root';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
boxSizing: 'border-box',
- width: '100%',
- height: 'auto',
- maxHeight: '550px',
- overflow: 'scroll',
+ overflow: 'auto',
padding: theme.spacing.unit
}
});
export interface CodeSnippetDataProps {
lines: string[];
+ className?: string;
}
type CodeSnippetProps = CodeSnippetDataProps & WithStyles<CssRules>;
export const CodeSnippet = withStyles(styles)(
- ({ classes, lines }: CodeSnippetProps) =>
- <Typography component="div" className={classes.root}>
+ ({ classes, lines, className }: CodeSnippetProps) =>
+ <Typography
+ component="div"
+ className={classNames(classes.root, className)}>
{
lines.map((line: string, index: number) => {
return <Typography key={index} component="pre">{line}</Typography>;
<CardHeader
className={classes.cardSubheader}
action={
- <IconButton onClick={onOptionsMenuOpen}>
- <Tooltip title="More options">
+ <Tooltip title="More options">
+ <IconButton onClick={onOptionsMenuOpen}>
<CustomizeTableIcon />
- </Tooltip>
- </IconButton>
+ </IconButton>
+ </Tooltip>
} />
<Grid container justify="space-between">
<Typography variant="caption" className={classes.nameHeader}>
export const ColumnSelector = withStyles(styles)(
({ columns, onColumnToggle, classes }: ColumnSelectorProps) =>
- <Popover triggerComponent={ColumnSelectorTrigger}>
- <Paper>
- <List dense>
- {columns
- .filter(column => column.configurable)
- .map((column, index) =>
- <ListItem
- button
- key={index}
- onClick={() => onColumnToggle(column)}>
- <Checkbox
- disableRipple
- color="primary"
- checked={column.selected}
- className={classes.checkbox} />
- <ListItemText>
- {column.name}
- </ListItemText>
- </ListItem>
- )}
- </List>
- </Paper>
- </Popover>
+ <Popover triggerComponent={ColumnSelectorTrigger}>
+ <Paper>
+ <List dense>
+ {columns
+ .filter(column => column.configurable)
+ .map((column, index) =>
+ <ListItem
+ button
+ key={index}
+ onClick={() => onColumnToggle(column)}>
+ <Checkbox
+ disableRipple
+ color="primary"
+ checked={column.selected}
+ className={classes.checkbox} />
+ <ListItemText>
+ {column.name}
+ </ListItemText>
+ </ListItem>
+ )}
+ </List>
+ </Paper>
+ </Popover>
);
export const ColumnSelectorTrigger = (props: IconButtonProps) =>
- <IconButton {...props}>
- <Tooltip title="Filters">
- <MenuIcon />
- </Tooltip>
- </IconButton>;
+ <Tooltip title="Filters">
+ <IconButton {...props}>
+ <MenuIcon aria-label="Filters" />
+ </IconButton>
+ </Tooltip>;
page: number;
contextMenuColumn: boolean;
dataTableDefaultView?: React.ReactNode;
+ working?: boolean;
}
interface DataExplorerActionProps<T> {
}
render() {
const {
- columns, onContextMenu, onFiltersChange, onSortToggle, extractKey,
+ columns, onContextMenu, onFiltersChange, onSortToggle, working, extractKey,
rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch,
items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
dataTableDefaultView
onFiltersChange={onFiltersChange}
onSortToggle={onSortToggle}
extractKey={extractKey}
+ working={working}
defaultView={dataTableDefaultView}
/>
<Toolbar>
renderContextMenuTrigger = (item: T) =>
<Grid container justify="flex-end">
- <IconButton onClick={event => this.props.onContextMenu(event, item)}>
- <Tooltip title="More options">
+ <Tooltip title="More options">
+ <IconButton onClick={event => this.props.onContextMenu(event, item)}>
<MoreVertIcon />
- </Tooltip>
- </IconButton>
+ </IconButton>
+ </Tooltip>
</Grid>
contextMenuColumn: DataColumn<any> = {
Card,
CardActions,
Typography,
- CardContent
+ CardContent,
+ Tooltip
} from "@material-ui/core";
import * as classnames from "classnames";
import { DefaultTransformOrigin } from "../popover/helpers";
const { name, classes, children } = this.props;
const isActive = this.state.filters.some(f => f.selected);
return <>
- <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 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}
onSortToggle: (column: DataColumn<T>) => void;
onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<T>) => void;
extractKey?: (item: T) => React.Key;
+ working?: boolean;
defaultView?: React.ReactNode;
}
{items.map(this.renderBodyRow)}
</TableBody>
</Table>
- {items.length === 0 && this.renderNoItemsPlaceholder()}
+ {items.length === 0 && this.props.working !== undefined && !this.props.working && this.renderNoItemsPlaceholder()}
</div>
</div>;
}
renderNoItemsPlaceholder = () => {
return this.props.defaultView
? this.props.defaultView
- : <DataTableDefaultView />;
+ : <DataTableDefaultView/>;
}
renderHeadCell = (column: DataColumn<T>, index: number) => {
}
},
typography: {
- fontFamily: 'VT323'
+ fontFamily: 'monospace'
}
});
-type DefaultCodeSnippet = CodeSnippetDataProps;
-
-export const DefaultCodeSnippet = (props: DefaultCodeSnippet) =>
+export const DefaultCodeSnippet = (props: CodeSnippetDataProps) =>
<MuiThemeProvider theme={theme}>
- <CodeSnippet lines={props.lines} />
+ <CodeSnippet {...props} />
</MuiThemeProvider>;
\ No newline at end of file
const { anchorEl } = this.state;
return (
<div>
- <IconButton
- aria-owns={anchorEl ? id : undefined}
- aria-haspopup="true"
- color="inherit"
- onClick={this.handleOpen}>
- <Tooltip title={title}>
+ <Tooltip title={title}>
+ <IconButton
+ aria-owns={anchorEl ? id : undefined}
+ aria-haspopup="true"
+ color="inherit"
+ onClick={this.handleOpen}>
{icon}
- </Tooltip>
- </IconButton>
+ </IconButton>
+ </Tooltip>
<Menu
id={id}
anchorEl={anchorEl}
<Typography
className={classes.sizeInfo}
variant="caption">{formatFileSize(item.data.size)}</Typography>
- <IconButton
- className={classes.button}
- onClick={this.handleClick}>
- <Tooltip title="More options">
+ <Tooltip title="More options">
+ <IconButton
+ className={classes.button}
+ onClick={this.handleClick}>
<MoreOptionsIcon />
- </Tooltip>
- </IconButton>
+ </IconButton>
+ </Tooltip>
</div >;
}
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';
import BubbleChart from '@material-ui/icons/BubbleChart';
export type IconType = React.SFC<{ className?: string }>;
+export const AddIcon: IconType = (props) => <Add {...props} />;
export const AddFavoriteIcon: IconType = (props) => <StarBorder {...props} />;
export const AdvancedIcon: IconType = (props) => <SettingsApplications {...props} />;
export const BackIcon: IconType = (props) => <ArrowBack {...props} />;
export const DownloadIcon: IconType = (props) => <GetApp {...props} />;
export const FavoriteIcon: IconType = (props) => <Star {...props} />;
export const HelpIcon: IconType = (props) => <Help {...props} />;
+export const HelpOutlineIcon: IconType = (props) => <HelpOutline {...props} />;
+export const ImportContactsIcon: IconType = (props) => <ImportContacts {...props} />;
export const InputIcon: IconType = (props) => <InsertDriveFile {...props} />;
export const LogIcon: IconType = (props) => <SettingsEthernet {...props} />;
+export const MailIcon: IconType = (props) => <Mail {...props} />;
export const MoreOptionsIcon: IconType = (props) => <MoreVert {...props} />;
export const MoveToIcon: IconType = (props) => <Input {...props} />;
export const NewProjectIcon: IconType = (props) => <CreateNewFolder {...props} />;
export const UserPanelIcon: IconType = (props) => <Person {...props} />;
export const UsedByIcon: IconType = (props) => <Folder {...props} />;
export const WorkflowIcon: IconType = (props) => <Code {...props} />;
-export const ImportContactsIcon: IconType = (props) => <ImportContacts {...props} />;
-export const HelpOutlineIcon: IconType = (props) => <HelpOutline {...props} />;
-export const MailIcon: IconType = (props) => <Mail {...props} />;
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { IconButton, Paper, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core';
+import { IconButton, Paper, StyleRulesCallback, withStyles, WithStyles, Tooltip } from '@material-ui/core';
import SearchIcon from '@material-ui/icons/Search';
type CssRules = 'container' | 'input' | 'button';
timeout: number;
render() {
- const {classes} = this.props;
+ const { classes } = this.props;
return <Paper className={classes.container}>
<form onSubmit={this.handleSubmit}>
<input
placeholder="Search"
value={this.state.value}
/>
- <IconButton className={classes.button}>
- <SearchIcon/>
- </IconButton>
+ <Tooltip title='Search'>
+ <IconButton className={classes.button}>
+ <SearchIcon />
+ </IconButton>
+ </Tooltip>
</form>
</Paper>;
}
componentDidMount() {
- this.setState({value: this.props.value});
+ this.setState({ value: this.props.value });
}
componentWillReceiveProps(nextProps: SearchBarProps) {
if (nextProps.value !== this.props.value) {
- this.setState({value: nextProps.value});
+ this.setState({ value: nextProps.value });
}
}
handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
clearTimeout(this.timeout);
- this.setState({value: event.target.value});
+ this.setState({ value: event.target.value });
this.timeout = window.setTimeout(
() => this.props.onSearch(this.state.value),
this.props.debounce || DEFAULT_SEARCH_DEBOUNCE
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { IconButton, StyleRulesCallback, withStyles, WithStyles, FormControl, InputLabel, Input, InputAdornment } from '@material-ui/core';
+import { IconButton, StyleRulesCallback, withStyles, WithStyles, FormControl, InputLabel, Input, InputAdornment, Tooltip } from '@material-ui/core';
import SearchIcon from '@material-ui/icons/Search';
type CssRules = 'container' | 'input' | 'button';
onChange={this.handleChange}
endAdornment={
<InputAdornment position="end">
- <IconButton
- onClick={this.handleSubmit}>
- <SearchIcon/>
- </IconButton>
+ <Tooltip title='Search'>
+ <IconButton
+ onClick={this.handleSubmit}>
+ <SearchIcon />
+ </IconButton>
+ </Tooltip>
</InputAdornment>
- }/>
+ } />
</FormControl>
</form>;
}
import { initWebSocket } from '~/websocket/websocket';
import { Config } from '~/common/config';
import { addRouteChangeHandlers } from './routes/route-change-handlers';
+import { setCurrentTokenDialogApiHost } from '~/store/current-token-dialog/current-token-dialog-actions';
+import { processResourceActionSet } from './views-components/context-menu/action-sets/process-resource-action-set';
+import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
+import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
const getBuildNumber = () => "BN-" + (process.env.REACT_APP_BUILD_NUMBER || "dev");
const getGitCommit = () => "GIT-" + (process.env.REACT_APP_GIT_COMMIT || "latest").substr(0, 7);
addMenuActionSet(ContextMenuKind.COLLECTION, collectionActionSet);
addMenuActionSet(ContextMenuKind.COLLECTION_RESOURCE, collectionResourceActionSet);
addMenuActionSet(ContextMenuKind.PROCESS, processActionSet);
+addMenuActionSet(ContextMenuKind.PROCESS_RESOURCE, processResourceActionSet);
addMenuActionSet(ContextMenuKind.TRASH, trashActionSet);
fetchConfig()
- .then((config) => {
+ .then(({ config, apiHost }) => {
const history = createBrowserHistory();
- const services = createServices(config);
+ const services = createServices(config, {
+ progressFn: (id, working) => {
+ store.dispatch(progressIndicatorActions.TOGGLE_WORKING({ id, working }));
+ },
+ errorFn: (id, error) => {
+ console.error("Backend error:", error);
+ store.dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Backend error", kind: SnackbarKind.ERROR }));
+ }
+ });
const store = configureStore(history, services);
store.subscribe(initListener(history, store, services, config));
store.dispatch(initAuth());
+ store.dispatch(setCurrentTokenDialogApiHost(apiHost));
const TokenComponent = (props: any) => <ApiToken authService={services.authService} {...props} />;
const WorkbenchComponent = (props: any) => <Workbench authService={services.authService} buildInfo={buildInfo} {...props} />;
<App />,
document.getElementById('root') as HTMLElement
);
-
-
});
const initListener = (history: History, store: RootStore, services: ServiceRepository, config: Config) => {
import { History, Location } from 'history';
import { RootStore } from '~/store/store';
-import { matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute, matchTrashRoute, matchRootRoute } from './routes';
+import { matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute, matchTrashRoute, matchRootRoute, matchSharedWithMeRoute } from './routes';
import { loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog } from '~/store/workbench/workbench-actions';
import { navigateToRootProject } from '~/store/navigation/navigation-action';
+import { navigateToSharedWithMe } from '../store/navigation/navigation-action';
+import { loadSharedWithMe } from '../store/workbench/workbench-actions';
export const addRouteChangeHandlers = (history: History, store: RootStore) => {
const handler = handleLocationChange(store);
const trashMatch = matchTrashRoute(pathname);
const processMatch = matchProcessRoute(pathname);
const processLogMatch = matchProcessLogRoute(pathname);
-
+ const sharedWithMeMatch = matchSharedWithMeRoute(pathname);
+
if (projectMatch) {
store.dispatch(loadProject(projectMatch.params.id));
} else if (collectionMatch) {
store.dispatch(loadProcessLog(processLogMatch.params.id));
} else if (rootMatch) {
store.dispatch(navigateToRootProject);
+ } else if (sharedWithMeMatch) {
+ store.dispatch(loadSharedWithMe);
}
};
PROCESSES: `/processes/:id(${RESOURCE_UUID_PATTERN})`,
FAVORITES: '/favorites',
TRASH: '/trash',
- PROCESS_LOGS: `/process-logs/:id(${RESOURCE_UUID_PATTERN})`
+ PROCESS_LOGS: `/process-logs/:id(${RESOURCE_UUID_PATTERN})`,
+ SHARED_WITH_ME: '/shared-with-me',
};
export const getResourceUrl = (uuid: string) => {
return getProjectUrl(uuid);
case ResourceKind.COLLECTION:
return getCollectionUrl(uuid);
+ case ResourceKind.PROCESS:
+ return getProcessUrl(uuid);
default:
return undefined;
}
export const matchProcessLogRoute = (route: string) =>
matchPath<ResourceRouteParams>(route, { path: Routes.PROCESS_LOGS });
+
+export const matchSharedWithMeRoute = (route: string) =>
+ matchPath(route, { path: Routes.SHARED_WITH_ME });
private userService: UserService
) { }
- async ancestors(uuid: string, rootUuid: string): Promise<Array<UserResource | GroupResource | TrashableResource>> {
+ async ancestors(uuid: string, rootUuid: string): Promise<Array<UserResource | GroupResource>> {
const service = this.getService(extractUuidObjectType(uuid));
if (service) {
- const resource = await service.get(uuid);
- if (uuid === rootUuid) {
- return [resource];
- } else {
- return [
- ...await this.ancestors(resource.ownerUuid, rootUuid),
- resource
- ];
+ try {
+ const resource = await service.get(uuid);
+ if (uuid === rootUuid) {
+ return [resource];
+ } else {
+ return [
+ ...await this.ancestors(resource.ownerUuid, rootUuid),
+ resource
+ ];
+ }
+ } catch (e) {
+ return [];
}
} else {
return [];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export type ProgressFn = (id: string, working: boolean) => void;
+export type ErrorFn = (id: string, error: any) => void;
+
+export interface ApiActions {
+ progressFn: ProgressFn;
+ errorFn: ErrorFn;
+}
filters = new FilterBuilder();
});
- it("should add 'equal' rule", () => {
+ it("should add 'equal' rule (string)", () => {
expect(
filters.addEqual("etag", "etagValue").getFilters()
).toEqual(`["etag","=","etagValue"]`);
});
+ it("should add 'equal' rule (boolean)", () => {
+ expect(
+ filters.addEqual("is_trashed", true).getFilters()
+ ).toEqual(`["is_trashed","=",true]`);
+ });
+
it("should add 'like' rule", () => {
expect(
filters.addLike("etag", "etagValue").getFilters()
export class FilterBuilder {
constructor(private filters = "") { }
- public addEqual(field: string, value?: string, resourcePrefix?: string) {
+ public addEqual(field: string, value?: string | boolean, resourcePrefix?: string) {
return this.addCondition(field, "=", value, "", "", resourcePrefix );
}
return this.filters;
}
- private addCondition(field: string, cond: string, value?: string | string[], prefix: string = "", postfix: string = "", resourcePrefix?: string) {
+ private addCondition(field: string, cond: string, value?: string | string[] | boolean, prefix: string = "", postfix: string = "", resourcePrefix?: string) {
if (value) {
- value = typeof value === "string"
- ? `"${prefix}${value}${postfix}"`
- : `["${value.join(`","`)}"]`;
+ if (typeof value === "string") {
+ value = `"${prefix}${value}${postfix}"`;
+ } else if (Array.isArray(value)) {
+ value = `["${value.join(`","`)}"]`;
+ } else {
+ value = value ? "true" : "false";
+ }
const resPrefix = resourcePrefix
? _.snakeCase(resourcePrefix) + "."
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { OrderBuilder } from "./order-builder";
+import { joinUrls } from "~/services/api/url-builder";
+
+describe("UrlBuilder", () => {
+ it("should join urls properly 1", () => {
+ expect(joinUrls('http://localhost:3000', '/main')).toEqual('http://localhost:3000/main');
+ });
+ it("should join urls properly 2", () => {
+ expect(joinUrls('http://localhost:3000/', '/main')).toEqual('http://localhost:3000/main');
+ });
+ it("should join urls properly 3", () => {
+ expect(joinUrls('http://localhost:3000//', '/main')).toEqual('http://localhost:3000/main');
+ });
+ it("should join urls properly 4", () => {
+ expect(joinUrls('http://localhost:3000', '//main')).toEqual('http://localhost:3000/main');
+ });
+ it("should join urls properly 5", () => {
+ expect(joinUrls('http://localhost:3000///', 'main')).toEqual('http://localhost:3000/main');
+ });
+ it("should join urls properly 6", () => {
+ expect(joinUrls('http://localhost:3000///', '//main')).toEqual('http://localhost:3000/main');
+ });
+ it("should join urls properly 7", () => {
+ expect(joinUrls(undefined, '//main')).toEqual('/main');
+ });
+ it("should join urls properly 8", () => {
+ expect(joinUrls(undefined, 'main')).toEqual('/main');
+ });
+ it("should join urls properly 9", () => {
+ expect(joinUrls('http://localhost:3000///', undefined)).toEqual('http://localhost:3000');
+ });
+});
return this.url + this.query;
}
}
+
+export function joinUrls(url0?: string, url1?: string) {
+ let u0 = "";
+ if (url0) {
+ let idx0 = url0.length - 1;
+ while (url0[idx0] === '/') { --idx0; }
+ u0 = url0.substr(0, idx0 + 1);
+ }
+ let u1 = "";
+ if (url1) {
+ let idx1 = 0;
+ while (url1[idx1] === '/') { ++idx1; }
+ u1 = url1.substr(idx1);
+ }
+ let url = u0;
+ if (u1.length > 0) {
+ url += '/';
+ }
+ url += u1;
+ return url;
+}
import { User } from "~/models/user";
import { AxiosInstance } from "axios";
+import { ApiActions, ProgressFn } from "~/services/api/api-actions";
+import * as uuid from "uuid/v4";
export const API_TOKEN_KEY = 'apiToken';
export const USER_EMAIL_KEY = 'userEmail';
constructor(
protected apiClient: AxiosInstance,
- protected baseUrl: string) { }
+ protected baseUrl: string,
+ protected actions: ApiActions) { }
public saveApiToken(token: string) {
localStorage.setItem(API_TOKEN_KEY, token);
}
public getUserDetails = (): Promise<User> => {
+ const reqId = uuid();
+ this.actions.progressFn(reqId, true);
return this.apiClient
.get<UserDetailsResponse>('/users/current')
- .then(resp => ({
- email: resp.data.email,
- firstName: resp.data.first_name,
- lastName: resp.data.last_name,
- uuid: resp.data.uuid,
- ownerUuid: resp.data.owner_uuid
- }));
+ .then(resp => {
+ this.actions.progressFn(reqId, false);
+ return {
+ email: resp.data.email,
+ firstName: resp.data.first_name,
+ lastName: resp.data.last_name,
+ uuid: resp.data.uuid,
+ ownerUuid: resp.data.owner_uuid
+ };
+ })
+ .catch(e => {
+ this.actions.progressFn(reqId, false);
+ this.actions.errorFn(reqId, e);
+ throw e;
+ });
}
public getRootUuid() {
import { parseFilesResponse } from "./collection-service-files-response";
import { fileToArrayBuffer } from "~/common/file";
import { TrashableResourceService } from "~/services/common-service/trashable-resource-service";
+import { ApiActions } from "~/services/api/api-actions";
export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
export class CollectionService extends TrashableResourceService<CollectionResource> {
- constructor(serverApi: AxiosInstance, private webdavClient: WebDAV, private authService: AuthService) {
- super(serverApi, "collections");
+ constructor(serverApi: AxiosInstance, private webdavClient: WebDAV, private authService: AuthService, actions: ApiActions) {
+ super(serverApi, "collections", actions);
}
async files(uuid: string) {
import axios, { AxiosInstance } from "axios";
import MockAdapter from "axios-mock-adapter";
import { Resource } from "src/models/resource";
+import { ApiActions } from "~/services/api/api-actions";
-export const mockResourceService = <R extends Resource, C extends CommonResourceService<R>>(Service: new (client: AxiosInstance) => C) => {
+const actions: ApiActions = {
+ progressFn: (id: string, working: boolean) => {},
+ errorFn: (id: string, message: string) => {}
+};
+
+export const mockResourceService = <R extends Resource, C extends CommonResourceService<R>>(
+ Service: new (client: AxiosInstance, actions: ApiActions) => C) => {
const axiosInstance = axios.create();
const axiosMock = new MockAdapter(axiosInstance);
- const service = new Service(axiosInstance);
+ const service = new Service(axiosInstance, actions);
Object.keys(service).map(key => service[key] = jest.fn());
return service;
};
.onPost("/resource/")
.reply(200, { owner_uuid: "ownerUuidValue" });
- const commonResourceService = new CommonResourceService(axiosInstance, "resource");
+ const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
const resource = await commonResourceService.create({ ownerUuid: "ownerUuidValue" });
expect(resource).toEqual({ ownerUuid: "ownerUuidValue" });
});
it("#create maps request params to snake case", async () => {
axiosInstance.post = jest.fn(() => Promise.resolve({data: {}}));
- const commonResourceService = new CommonResourceService(axiosInstance, "resource");
+ const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
await commonResourceService.create({ ownerUuid: "ownerUuidValue" });
expect(axiosInstance.post).toHaveBeenCalledWith("/resource/", {owner_uuid: "ownerUuidValue"});
});
.onDelete("/resource/uuid")
.reply(200, { deleted_at: "now" });
- const commonResourceService = new CommonResourceService(axiosInstance, "resource");
+ const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
const resource = await commonResourceService.delete("uuid");
expect(resource).toEqual({ deletedAt: "now" });
});
.onGet("/resource/uuid")
.reply(200, { modified_at: "now" });
- const commonResourceService = new CommonResourceService(axiosInstance, "resource");
+ const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
const resource = await commonResourceService.get("uuid");
expect(resource).toEqual({ modifiedAt: "now" });
});
items_available: 20
});
- const commonResourceService = new CommonResourceService(axiosInstance, "resource");
+ const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
const resource = await commonResourceService.list({ limit: 10, offset: 1 });
expect(resource).toEqual({
kind: "kind",
import * as _ from "lodash";
import { AxiosInstance, AxiosPromise } from "axios";
import { Resource } from "src/models/resource";
+import * as uuid from "uuid/v4";
+import { ApiActions } from "~/services/api/api-actions";
export interface ListArguments {
limit?: number;
export enum CommonResourceServiceError {
UNIQUE_VIOLATION = 'UniqueViolation',
OWNERSHIP_CYCLE = 'OwnershipCycle',
+ MODIFYING_CONTAINER_REQUEST_FINAL_STATE = 'ModifyingContainerRequestFinalState',
UNKNOWN = 'Unknown',
NONE = 'None'
}
export class CommonResourceService<T extends Resource> {
- static mapResponseKeys = (response: { data: any }): Promise<any> =>
+ static mapResponseKeys = (response: { data: any }) =>
CommonResourceService.mapKeys(_.camelCase)(response.data)
static mapKeys = (mapFn: (key: string) => string) =>
}
}
- static defaultResponse<R>(promise: AxiosPromise<R>): Promise<R> {
+ static defaultResponse<R>(promise: AxiosPromise<R>, actions: ApiActions): Promise<R> {
+ const reqId = uuid();
+ actions.progressFn(reqId, true);
return promise
+ .then(data => {
+ actions.progressFn(reqId, false);
+ return data;
+ })
.then(CommonResourceService.mapResponseKeys)
- .catch(({ response }) => Promise.reject<Errors>(CommonResourceService.mapResponseKeys(response)));
+ .catch(({ response }) => {
+ actions.progressFn(reqId, false);
+ const errors = CommonResourceService.mapResponseKeys(response) as Errors;
+ actions.errorFn(reqId, errors);
+ throw errors;
+ });
}
protected serverApi: AxiosInstance;
protected resourceType: string;
+ protected actions: ApiActions;
- constructor(serverApi: AxiosInstance, resourceType: string) {
+ constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions) {
this.serverApi = serverApi;
this.resourceType = '/' + resourceType + '/';
+ this.actions = actions;
}
create(data?: Partial<T> | any) {
return CommonResourceService.defaultResponse(
this.serverApi
- .post<T>(this.resourceType, data && CommonResourceService.mapKeys(_.snakeCase)(data)));
+ .post<T>(this.resourceType, data && CommonResourceService.mapKeys(_.snakeCase)(data)),
+ this.actions
+ );
}
delete(uuid: string): Promise<T> {
return CommonResourceService.defaultResponse(
this.serverApi
- .delete(this.resourceType + uuid));
+ .delete(this.resourceType + uuid),
+ this.actions
+ );
}
get(uuid: string) {
return CommonResourceService.defaultResponse(
this.serverApi
- .get<T>(this.resourceType + uuid));
+ .get<T>(this.resourceType + uuid),
+ this.actions
+ );
}
list(args: ListArguments = {}): Promise<ListResults<T>> {
this.serverApi
.get(this.resourceType, {
params: CommonResourceService.mapKeys(_.snakeCase)(params)
- }));
+ }),
+ this.actions
+ );
}
update(uuid: string, data: Partial<T>) {
return CommonResourceService.defaultResponse(
this.serverApi
- .put<T>(this.resourceType + uuid, data && CommonResourceService.mapKeys(_.snakeCase)(data)));
-
+ .put<T>(this.resourceType + uuid, data && CommonResourceService.mapKeys(_.snakeCase)(data)),
+ this.actions
+ );
}
}
return CommonResourceServiceError.UNIQUE_VIOLATION;
case /ownership cycle/.test(error):
return CommonResourceServiceError.OWNERSHIP_CYCLE;
+ case /Mounts cannot be modified in state 'Final'/.test(error):
+ return CommonResourceServiceError.MODIFYING_CONTAINER_REQUEST_FINAL_STATE;
default:
return CommonResourceServiceError.UNKNOWN;
}
import { AxiosInstance } from "axios";
import { TrashableResource } from "src/models/resource";
import { CommonResourceService } from "~/services/common-service/common-resource-service";
+import { ApiActions } from "~/services/api/api-actions";
export class TrashableResourceService<T extends TrashableResource> extends CommonResourceService<T> {
- constructor(serverApi: AxiosInstance, resourceType: string) {
- super(serverApi, resourceType);
+ constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions) {
+ super(serverApi, resourceType, actions);
}
trash(uuid: string): Promise<T> {
- return this.serverApi
- .post(this.resourceType + `${uuid}/trash`)
- .then(CommonResourceService.mapResponseKeys);
+ return CommonResourceService.defaultResponse(
+ this.serverApi
+ .post(this.resourceType + `${uuid}/trash`),
+ this.actions
+ );
}
untrash(uuid: string): Promise<T> {
const params = {
ensure_unique_name: true
};
- return this.serverApi
- .post(this.resourceType + `${uuid}/untrash`, {
- params: CommonResourceService.mapKeys(_.snakeCase)(params)
- })
- .then(CommonResourceService.mapResponseKeys);
+ return CommonResourceService.defaultResponse(
+ this.serverApi
+ .post(this.resourceType + `${uuid}/untrash`, {
+ params: CommonResourceService.mapKeys(_.snakeCase)(params)
+ }),
+ this.actions
+ );
}
}
import { CommonResourceService } from "~/services/common-service/common-resource-service";
import { AxiosInstance } from "axios";
-import { ContainerRequestResource } from '../../models/container-request';
+import { ContainerRequestResource } from '~/models/container-request';
+import { ApiActions } from "~/services/api/api-actions";
export class ContainerRequestService extends CommonResourceService<ContainerRequestResource> {
- constructor(serverApi: AxiosInstance) {
- super(serverApi, "container_requests");
+ constructor(serverApi: AxiosInstance, actions: ApiActions) {
+ super(serverApi, "container_requests", actions);
}
}
import { CommonResourceService } from "~/services/common-service/common-resource-service";
import { AxiosInstance } from "axios";
-import { ContainerResource } from '../../models/container';
+import { ContainerResource } from '~/models/container';
+import { ApiActions } from "~/services/api/api-actions";
export class ContainerService extends CommonResourceService<ContainerResource> {
- constructor(serverApi: AxiosInstance) {
- super(serverApi, "containers");
+ constructor(serverApi: AxiosInstance, actions: ApiActions) {
+ super(serverApi, "containers", actions);
}
}
export class FavoriteService {
constructor(
private linkService: LinkService,
- private groupsService: GroupsService
+ private groupsService: GroupsService,
) {}
create(data: { userUuid: string; resource: { uuid: string; name: string } }) {
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { GroupsService } from "./groups-service";
+import { ApiActions } from "~/services/api/api-actions";
describe("GroupsService", () => {
const axiosMock = new MockAdapter(axios);
+ const actions: ApiActions = {
+ progressFn: (id: string, working: boolean) => {},
+ errorFn: (id: string, message: string) => {}
+ };
+
beforeEach(() => {
axiosMock.reset();
});
items_available: 20
});
- const groupsService = new GroupsService(axios);
+ const groupsService = new GroupsService(axios, actions);
const resource = await groupsService.contents("1", { limit: 10, offset: 1 });
expect(resource).toEqual({
kind: "kind",
// SPDX-License-Identifier: AGPL-3.0
import * as _ from "lodash";
-import { CommonResourceService, ListResults } from "~/services/common-service/common-resource-service";
+import { CommonResourceService, ListResults, ListArguments } from '~/services/common-service/common-resource-service';
import { AxiosInstance } from "axios";
import { CollectionResource } from "~/models/collection";
import { ProjectResource } from "~/models/project";
import { ProcessResource } from "~/models/process";
import { TrashableResource } from "~/models/resource";
import { TrashableResourceService } from "~/services/common-service/trashable-resource-service";
+import { ApiActions } from "~/services/api/api-actions";
+import { GroupResource } from "~/models/group";
export interface ContentsArguments {
limit?: number;
includeTrash?: boolean;
}
+export interface SharedArguments extends ListArguments {
+ include?: string;
+}
+
export type GroupContentsResource =
CollectionResource |
ProjectResource |
ProcessResource;
-export class GroupsService<T extends TrashableResource = TrashableResource> extends TrashableResourceService<T> {
+export class GroupsService<T extends GroupResource = GroupResource> extends TrashableResourceService<T> {
- constructor(serverApi: AxiosInstance) {
- super(serverApi, "groups");
+ constructor(serverApi: AxiosInstance, actions: ApiActions) {
+ super(serverApi, "groups", actions);
}
contents(uuid: string, args: ContentsArguments = {}): Promise<ListResults<GroupContentsResource>> {
filters: filters ? `[${filters}]` : undefined,
order: order ? order : undefined
};
- return this.serverApi
- .get(this.resourceType + `${uuid}/contents`, {
- params: CommonResourceService.mapKeys(_.snakeCase)(params)
- })
- .then(CommonResourceService.mapResponseKeys);
+ return CommonResourceService.defaultResponse(
+ this.serverApi
+ .get(this.resourceType + `${uuid}/contents`, {
+ params: CommonResourceService.mapKeys(_.snakeCase)(params)
+ }),
+ this.actions
+ );
+ }
+
+ shared(params: SharedArguments = {}): Promise<ListResults<GroupContentsResource>> {
+ return CommonResourceService.defaultResponse(
+ this.serverApi
+ .get(this.resourceType + 'shared', { params }),
+ this.actions
+ );
}
}
import { CommonResourceService } from "~/services/common-service/common-resource-service";\r
import { AxiosInstance } from "axios";\r
import { KeepResource } from "~/models/keep";\r
+import { ApiActions } from "~/services/api/api-actions";\r
\r
export class KeepService extends CommonResourceService<KeepResource> {\r
- constructor(serverApi: AxiosInstance) {\r
- super(serverApi, "keep_services");\r
+ constructor(serverApi: AxiosInstance, actions: ApiActions) {\r
+ super(serverApi, "keep_services", actions);\r
}\r
}\r
import { CommonResourceService } from "~/services/common-service/common-resource-service";
import { LinkResource } from "~/models/link";
import { AxiosInstance } from "axios";
+import { ApiActions } from "~/services/api/api-actions";
export class LinkService extends CommonResourceService<LinkResource> {
- constructor(serverApi: AxiosInstance) {
- super(serverApi, "links");
+ constructor(serverApi: AxiosInstance, actions: ApiActions) {
+ super(serverApi, "links", actions);
}
}
import { AxiosInstance } from "axios";
import { LogResource } from '~/models/log';
import { CommonResourceService } from "~/services/common-service/common-resource-service";
+import { ApiActions } from "~/services/api/api-actions";
export class LogService extends CommonResourceService<LogResource> {
- constructor(serverApi: AxiosInstance) {
- super(serverApi, "logs");
+ constructor(serverApi: AxiosInstance, actions: ApiActions) {
+ super(serverApi, "logs", actions);
}
}
import axios from "axios";
import { ProjectService } from "./project-service";
import { FilterBuilder } from "~/services/api/filter-builder";
+import { ApiActions } from "~/services/api/api-actions";
describe("CommonResourceService", () => {
const axiosInstance = axios.create();
+ const actions: ApiActions = {
+ progressFn: (id: string, working: boolean) => {},
+ errorFn: (id: string, message: string) => {}
+ };
it(`#create has groupClass set to "project"`, async () => {
axiosInstance.post = jest.fn(() => Promise.resolve({ data: {} }));
- const projectService = new ProjectService(axiosInstance);
+ const projectService = new ProjectService(axiosInstance, actions);
const resource = await projectService.create({ name: "nameValue" });
expect(axiosInstance.post).toHaveBeenCalledWith("/groups/", {
name: "nameValue",
it("#list has groupClass filter set by default", async () => {
axiosInstance.get = jest.fn(() => Promise.resolve({ data: {} }));
- const projectService = new ProjectService(axiosInstance);
+ const projectService = new ProjectService(axiosInstance, actions);
const resource = await projectService.list();
expect(axiosInstance.get).toHaveBeenCalledWith("/groups/", {
params: {
import { TagService } from "./tag-service/tag-service";
import { CollectionFilesService } from "./collection-files-service/collection-files-service";
import { KeepService } from "./keep-service/keep-service";
-import { WebDAV } from "../common/webdav";
-import { Config } from "../common/config";
+import { WebDAV } from "~/common/webdav";
+import { Config } from "~/common/config";
import { UserService } from './user-service/user-service';
import { AncestorService } from "~/services/ancestors-service/ancestors-service";
import { ResourceKind } from "~/models/resource";
import { ContainerRequestService } from './container-request-service/container-request-service';
import { ContainerService } from './container-service/container-service';
import { LogService } from './log-service/log-service';
+import { ApiActions } from "~/services/api/api-actions";
export type ServiceRepository = ReturnType<typeof createServices>;
-export const createServices = (config: Config) => {
+export const createServices = (config: Config, actions: ApiActions) => {
const apiClient = Axios.create();
apiClient.defaults.baseURL = config.baseUrl;
const webdavClient = new WebDAV();
webdavClient.defaults.baseURL = config.keepWebServiceUrl;
- const containerRequestService = new ContainerRequestService(apiClient);
- const containerService = new ContainerService(apiClient);
- const groupsService = new GroupsService(apiClient);
- const keepService = new KeepService(apiClient);
- const linkService = new LinkService(apiClient);
- const logService = new LogService(apiClient);
- const projectService = new ProjectService(apiClient);
- const userService = new UserService(apiClient);
-
+ const containerRequestService = new ContainerRequestService(apiClient, actions);
+ const containerService = new ContainerService(apiClient, actions);
+ const groupsService = new GroupsService(apiClient, actions);
+ const keepService = new KeepService(apiClient, actions);
+ const linkService = new LinkService(apiClient, actions);
+ const logService = new LogService(apiClient, actions);
+ const projectService = new ProjectService(apiClient, actions);
+ const userService = new UserService(apiClient, actions);
+
const ancestorsService = new AncestorService(groupsService, userService);
- const authService = new AuthService(apiClient, config.rootUrl);
- const collectionService = new CollectionService(apiClient, webdavClient, authService);
+ const authService = new AuthService(apiClient, config.rootUrl, actions);
+ const collectionService = new CollectionService(apiClient, webdavClient, authService, actions);
const collectionFilesService = new CollectionFilesService(collectionService);
const favoriteService = new FavoriteService(linkService, groupsService);
const tagService = new TagService(linkService);
default:
return undefined;
}
-};
\ No newline at end of file
+};
import { AxiosInstance } from "axios";
import { CommonResourceService } from "~/services/common-service/common-resource-service";
import { UserResource } from "~/models/user";
+import { ApiActions } from "~/services/api/api-actions";
export class UserService extends CommonResourceService<UserResource> {
- constructor(serverApi: AxiosInstance) {
- super(serverApi, "users");
+ constructor(serverApi: AxiosInstance, actions: ApiActions) {
+ super(serverApi, "users", actions);
}
}
import { configureStore, RootStore } from "../store";
import createBrowserHistory from "history/createBrowserHistory";
import { mockConfig } from '~/common/config';
+import { ApiActions } from "~/services/api/api-actions";
describe('auth-actions', () => {
let reducer: (state: AuthState | undefined, action: AuthAction) => any;
let store: RootStore;
+ const actions: ApiActions = {
+ progressFn: (id: string, working: boolean) => {},
+ errorFn: (id: string, message: string) => {}
+ };
beforeEach(() => {
- store = configureStore(createBrowserHistory(), createServices(mockConfig({})));
+ store = configureStore(createBrowserHistory(), createServices(mockConfig({}), actions));
localStorage.clear();
- reducer = authReducer(createServices(mockConfig({})));
+ reducer = authReducer(createServices(mockConfig({}), actions));
});
it('should initialise state with user and api token from local storage', () => {
import 'jest-localstorage-mock';
import { createServices } from "~/services/services";
import { mockConfig } from '~/common/config';
+import { ApiActions } from "~/services/api/api-actions";
describe('auth-reducer', () => {
let reducer: (state: AuthState | undefined, action: AuthAction) => any;
+ const actions: ApiActions = {
+ progressFn: (id: string, working: boolean) => {},
+ errorFn: (id: string, message: string) => {}
+ };
beforeAll(() => {
localStorage.clear();
- reducer = authReducer(createServices(mockConfig({})));
+ reducer = authReducer(createServices(mockConfig({}), actions));
});
it('should correctly initialise state', () => {
import { Breadcrumb } from '~/components/breadcrumbs/breadcrumbs';
import { getResource } from '~/store/resources/resources';
import { TreePicker } from '../tree-picker/tree-picker';
-import { getSidePanelTreeBranch } from '../side-panel-tree/side-panel-tree-actions';
+import { getSidePanelTreeBranch, getSidePanelTreeNodeAncestorsIds } from '../side-panel-tree/side-panel-tree-actions';
import { propertiesActions } from '../properties/properties-actions';
import { getProcess } from '~/store/processes/process';
+import { ServiceRepository } from '~/services/services';
+import { SidePanelTreeCategory, activateSidePanelTreeItem } from '~/store/side-panel-tree/side-panel-tree-actions';
+import { updateResources } from '../resources/resources-actions';
+import { ResourceKind } from '~/models/resource';
export const BREADCRUMBS = 'breadcrumbs';
dispatch(setBreadcrumbs(breadcrumbs));
};
-export const setProjectBreadcrumbs = setSidePanelBreadcrumbs;
+export const setSharedWithMeBreadcrumbs = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const ancestors = await services.ancestorsService.ancestors(uuid, '');
+ dispatch(updateResources(ancestors));
+ const initialBreadcrumbs: ResourceBreadcrumb[] = [
+ { label: SidePanelTreeCategory.SHARED_WITH_ME, uuid: SidePanelTreeCategory.SHARED_WITH_ME }
+ ];
+ const breadrumbs = ancestors.reduce((breadcrumbs, ancestor) =>
+ ancestor.kind === ResourceKind.GROUP
+ ? [...breadcrumbs, { label: ancestor.name, uuid: ancestor.uuid }]
+ : breadcrumbs,
+ initialBreadcrumbs);
+
+ dispatch(setBreadcrumbs(breadrumbs));
+ };
+
+export const setProjectBreadcrumbs = (uuid: string) =>
+ (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ const ancestors = getSidePanelTreeNodeAncestorsIds(uuid)(getState().treePicker);
+ const rootUuid = services.authService.getUuid();
+ if (uuid === rootUuid ||ancestors.find(uuid => uuid === rootUuid)) {
+ dispatch(setSidePanelBreadcrumbs(uuid));
+ } else {
+ dispatch(setSharedWithMeBreadcrumbs(uuid));
+ dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.SHARED_WITH_ME));
+ }
+ };
export const setCollectionBreadcrumbs = (collectionUuid: string) =>
(dispatch: Dispatch, getState: () => RootState) => {
import { RootState } from '~/store/store';
import { ServiceRepository } from '~/services/services';
import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
export const COLLECTION_COPY_FORM_NAME = 'collectionCopyFormName';
-export interface CollectionCopyFormDialogData {
- name: string;
- ownerUuid: string;
- uuid: string;
-}
-
export const openCollectionCopyDialog = (resource: { name: string, uuid: string }) =>
(dispatch: Dispatch) => {
dispatch<any>(resetPickerProjectTree());
- const initialData: CollectionCopyFormDialogData = { name: `Copy of: ${resource.name}`, ownerUuid: '', uuid: resource.uuid };
+ const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, ownerUuid: '', uuid: resource.uuid };
dispatch<any>(initialize(COLLECTION_COPY_FORM_NAME, initialData));
dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_COPY_FORM_NAME, data: {} }));
};
-export const copyCollection = (resource: CollectionCopyFormDialogData) =>
+export const copyCollection = (resource: CopyFormDialogData) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(startSubmit(COLLECTION_COPY_FORM_NAME));
try {
+ dispatch(progressIndicatorActions.START_WORKING(COLLECTION_COPY_FORM_NAME));
const collection = await services.collectionService.get(resource.uuid);
const uuidKey = 'uuid';
delete collection[uuidKey];
await services.collectionService.create({ ...collection, ownerUuid: resource.ownerUuid, name: resource.name });
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_COPY_FORM_NAME }));
+ dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_COPY_FORM_NAME));
return collection;
} catch (e) {
const error = getCommonResourceServiceError(e);
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_COPY_FORM_NAME }));
throw new Error('Could not copy the collection.');
}
- return ;
+ dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_COPY_FORM_NAME));
+ return;
}
};
import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
import { uploadCollectionFiles } from './collection-upload-actions';
import { fileUploaderActions } from '~/store/file-uploader/file-uploader-actions';
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
export interface CollectionCreateFormDialogData {
ownerUuid: string;
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(startSubmit(COLLECTION_CREATE_FORM_NAME));
try {
+ dispatch(progressIndicatorActions.START_WORKING(COLLECTION_CREATE_FORM_NAME));
const newCollection = await services.collectionService.create(data);
await dispatch<any>(uploadCollectionFiles(newCollection.uuid));
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_CREATE_FORM_NAME }));
dispatch(reset(COLLECTION_CREATE_FORM_NAME));
+ dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_CREATE_FORM_NAME));
return newCollection;
} catch (e) {
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
dispatch(stopSubmit(COLLECTION_CREATE_FORM_NAME, { name: 'Collection with the same name already exists.' }));
}
- return ;
+ dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_CREATE_FORM_NAME));
+ return;
}
};
import { ServiceRepository } from '~/services/services';
import { RootState } from '~/store/store';
import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
-import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
import { projectPanelActions } from '~/store/project-panel/project-panel-action';
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
export const COLLECTION_MOVE_FORM_NAME = 'collectionMoveFormName';
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(startSubmit(COLLECTION_MOVE_FORM_NAME));
try {
+ dispatch(progressIndicatorActions.START_WORKING(COLLECTION_MOVE_FORM_NAME));
const collection = await services.collectionService.get(resource.uuid);
await services.collectionService.update(resource.uuid, { ...collection, ownerUuid: resource.ownerUuid });
dispatch(projectPanelActions.REQUEST_ITEMS());
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_MOVE_FORM_NAME }));
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection has been moved', hideDuration: 2000 }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: 'Collection has been moved',
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS
+ }));
+ dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_MOVE_FORM_NAME));
return collection;
} catch (e) {
const error = getCommonResourceServiceError(e);
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_MOVE_FORM_NAME }));
dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not move the collection.', hideDuration: 2000 }));
}
- return ;
+ dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_MOVE_FORM_NAME));
+ return;
}
};
import { dialogActions } from '~/store/dialog/dialog-actions';
import { ServiceRepository } from '~/services/services';
import { filterCollectionFilesBySelection } from '../collection-panel/collection-panel-files/collection-panel-files-state';
-import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
export const COLLECTION_PARTIAL_COPY_FORM_NAME = 'COLLECTION_PARTIAL_COPY_DIALOG';
const currentCollection = state.collectionPanel.item;
if (currentCollection) {
try {
+ dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_COPY_FORM_NAME));
const collection = await services.collectionService.get(currentCollection.uuid);
const collectionCopy = {
...collection,
const paths = filterCollectionFilesBySelection(state.collectionPanelFiles, false).map(file => file.id);
await services.collectionService.deleteFiles(newCollection.uuid, paths);
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME }));
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'New collection created.', hideDuration: 2000 }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: 'New collection created.',
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS
+ }));
+ dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PARTIAL_COPY_FORM_NAME));
} catch (e) {
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME }));
dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection has been copied but may contain incorrect files.', hideDuration: 2000 }));
}
+ dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PARTIAL_COPY_FORM_NAME));
}
}
};
import { ServiceRepository } from "~/services/services";
import { CollectionResource } from '~/models/collection';
import { ContextMenuResource } from "~/store/context-menu/context-menu-actions";
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
export interface CollectionUpdateFormDialogData {
uuid: string;
const uuid = collection.uuid || '';
dispatch(startSubmit(COLLECTION_UPDATE_FORM_NAME));
try {
+ dispatch(progressIndicatorActions.START_WORKING(COLLECTION_UPDATE_FORM_NAME));
const updatedCollection = await services.collectionService.update(uuid, collection);
dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: updatedCollection as CollectionResource }));
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_UPDATE_FORM_NAME }));
+ dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPDATE_FORM_NAME));
return updatedCollection;
} catch (e) {
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
dispatch(stopSubmit(COLLECTION_UPDATE_FORM_NAME, { name: 'Collection with the same name already exists.' }));
}
+ dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPDATE_FORM_NAME));
return;
}
};
import { ServiceRepository } from '~/services/services';
import { dialogActions } from '~/store/dialog/dialog-actions';
import { loadCollectionFiles } from '../collection-panel/collection-panel-files/collection-panel-files-actions';
-import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
import { fileUploaderActions } from '~/store/file-uploader/file-uploader-actions';
import { reset, startSubmit } from 'redux-form';
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
export const uploadCollectionFiles = (collectionUuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const currentCollection = getState().collectionPanel.item;
if (currentCollection) {
- dispatch(startSubmit(COLLECTION_UPLOAD_FILES_DIALOG));
- await dispatch<any>(uploadCollectionFiles(currentCollection.uuid));
- dispatch<any>(loadCollectionFiles(currentCollection.uuid));
- dispatch(closeUploadCollectionFilesDialog());
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Data has been uploaded.', hideDuration: 2000 }));
+ try {
+ dispatch(progressIndicatorActions.START_WORKING(COLLECTION_UPLOAD_FILES_DIALOG));
+ dispatch(startSubmit(COLLECTION_UPLOAD_FILES_DIALOG));
+ await dispatch<any>(uploadCollectionFiles(currentCollection.uuid));
+ dispatch<any>(loadCollectionFiles(currentCollection.uuid));
+ dispatch(closeUploadCollectionFilesDialog());
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: 'Data has been uploaded.',
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS
+ }));
+ dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPLOAD_FILES_DIALOG));
+ } catch (e) {
+ dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPLOAD_FILES_DIALOG));
+ }
}
};
const handleUploadProgress = (dispatch: Dispatch) => (fileId: number, loaded: number, total: number, currentTime: number) => {
dispatch(fileUploaderActions.SET_UPLOAD_PROGRESS({ fileId, loaded, total, currentTime }));
-};
\ No newline at end of file
+};
import { isSidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
import { extractUuidKind, ResourceKind } from '~/models/resource';
import { matchProcessRoute } from '~/routes/routes';
+import { Process } from '~/store/processes/process';
export const contextMenuActions = unionize({
OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
}
};
-export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>) =>
+export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, process: Process) =>
(dispatch: Dispatch, getState: () => RootState) => {
- const { location } = getState().router;
- const pathname = location ? location.pathname : '';
- const match = matchProcessRoute(pathname);
- const uuid = match ? match.params.id : '';
const resource = {
- uuid,
+ uuid: process.containerRequest.uuid,
ownerUuid: '',
kind: ResourceKind.PROCESS,
name: '',
return ContextMenuKind.PROJECT;
case ResourceKind.COLLECTION:
return ContextMenuKind.COLLECTION_RESOURCE;
+ case ResourceKind.PROCESS:
+ return ContextMenuKind.PROCESS_RESOURCE;
case ResourceKind.USER:
return ContextMenuKind.ROOT_PROJECT;
default:
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export interface CopyFormDialogData {
+ name: string;
+ uuid: string;
+ ownerUuid: string;
+}
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { getProperty } from '../properties/properties';
+import { propertiesActions } from '~/store/properties/properties-actions';
+import { RootState } from '~/store/store';
+
+export const CURRENT_TOKEN_DIALOG_NAME = 'currentTokenDialog';
+const API_HOST_PROPERTY_NAME = 'apiHost';
+
+export interface CurrentTokenDialogData {
+ currentToken: string;
+ apiHost: string;
+}
+
+export const setCurrentTokenDialogApiHost = (apiHost: string) =>
+ propertiesActions.SET_PROPERTY({ key: API_HOST_PROPERTY_NAME, value: apiHost });
+
+export const getCurrentTokenDialogData = (state: RootState): CurrentTokenDialogData => ({
+ apiHost: getProperty<string>(API_HOST_PROPERTY_NAME)(state.properties) || '',
+ currentToken: state.auth.apiToken || '',
+});
+
+export const openCurrentTokenDialog = dialogActions.OPEN_DIALOG({ id: CURRENT_TOKEN_DIALOG_NAME, data: {} });
rowsPerPage: number;
rowsPerPageOptions: number[];
searchValue: string;
+ working?: boolean;
}
export const initialDataExplorer: DataExplorer = {
import { LinkResource } from "~/models/link";
import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
import { resourcesActions } from "~/store/resources/resources-actions";
-import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions.ts';
import { getDataExplorer } from "~/store/data-explorer/data-explorer-reducer";
import { loadMissingProcessesInformation } from "~/store/project-panel/project-panel-middleware-service";
if (!dataExplorer) {
api.dispatch(favoritesPanelDataExplorerIsNotSet());
} else {
-
const columns = dataExplorer.columns as DataColumns<string, FavoritePanelFilter>;
const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
const typeFilters = this.getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
.addOrder(direction, "name", GroupContentsResourcePrefix.PROJECT);
}
try {
+ api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
const response = await this.services.favoriteService
.list(this.services.authService.getUuid()!, {
limit: dataExplorer.rowsPerPage,
.addILike("name", dataExplorer.searchValue)
.getFilters()
});
+ api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
api.dispatch(resourcesActions.SET_RESOURCES(response.items));
await api.dispatch<any>(loadMissingProcessesInformation(response.items));
api.dispatch(favoritePanelActions.SET_ITEMS({
}));
api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
} catch (e) {
+ api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
api.dispatch(favoritePanelActions.SET_ITEMS({
items: [],
itemsAvailable: 0,
page: 0,
rowsPerPage: dataExplorer.rowsPerPage
}));
+ api.dispatch(couldNotFetchFavoritesContents());
}
}
}
snackbarActions.OPEN_SNACKBAR({
message: 'Favorites panel is not ready.'
});
+
+const couldNotFetchFavoritesContents = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Could not fetch favorites contents.',
+ kind: SnackbarKind.ERROR
+ });
import { Dispatch } from "redux";
import { RootState } from "../store";
import { checkFavorite } from "./favorites-reducer";
-import { snackbarActions } from "../snackbar/snackbar-actions";
+import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
import { ServiceRepository } from "~/services/services";
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
export const favoritesActions = unionize({
TOGGLE_FAVORITE: ofType<{ resourceUuid: string }>(),
export const toggleFavorite = (resource: { uuid: string; name: string }) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+ dispatch(progressIndicatorActions.START_WORKING("toggleFavorite"));
const userUuid = getState().auth.user!.uuid;
dispatch(favoritesActions.TOGGLE_FAVORITE({ resourceUuid: resource.uuid }));
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Working..." }));
const isFavorite = checkFavorite(resource.uuid, getState().favorites);
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: isFavorite
+ ? "Removing from favorites..."
+ : "Adding to favorites..."
+ }));
+
const promise: any = isFavorite
? services.favoriteService.delete({ userUuid, resourceUuid: resource.uuid })
: services.favoriteService.create({ userUuid, resource });
message: isFavorite
? "Removed from favorites"
: "Added to favorites",
- hideDuration: 2000
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS
}));
+ dispatch(progressIndicatorActions.STOP_WORKING("toggleFavorite"));
+ })
+ .catch((e: any) => {
+ dispatch(progressIndicatorActions.STOP_WORKING("toggleFavorite"));
+ throw e;
});
};
}
if (uuid === SidePanelTreeCategory.FAVORITES) {
dispatch<any>(navigateToFavorites);
+ } else if(uuid === SidePanelTreeCategory.SHARED_WITH_ME){
+ dispatch(navigateToSharedWithMe);
}
};
if (rootProjectUuid) {
dispatch(navigateToProject(rootProjectUuid));
}
-};
\ No newline at end of file
+};
+
+export const navigateToSharedWithMe = push(Routes.SHARED_WITH_ME);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { dialogActions } from '~/store/dialog/dialog-actions';
+import { RootState } from '../store';
+import { Dispatch } from 'redux';
+import { getProcess } from '~/store/processes/process';
+
+export const PROCESS_COMMAND_DIALOG_NAME = 'processCommandDialog';
+
+export interface ProcessCommandDialogData {
+ command: string;
+ processName: string;
+}
+
+export const openProcessCommandDialog = (processUuid: string) =>
+ (dispatch: Dispatch<any>, getState: () => RootState) => {
+ const process = getProcess(processUuid)(getState().resources);
+ if (process) {
+ const data: ProcessCommandDialogData = {
+ command: process.containerRequest.command.join(' '),
+ processName: process.containerRequest.name,
+ };
+ dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_COMMAND_DIALOG_NAME, data }));
+ }
+ };
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { initialize, startSubmit } from 'redux-form';
+import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
+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 { snackbarActions } from '~/store/snackbar/snackbar-actions';
+
+export const PROCESS_COPY_FORM_NAME = 'processCopyFormName';
+
+export const openCopyProcessDialog = (resource: { name: string, uuid: string }) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const process = getProcess(resource.uuid)(getState().resources);
+ if (process) {
+ const processStatus = getProcessStatus(process);
+ if (processStatus === ProcessStatus.DRAFT) {
+ dispatch<any>(resetPickerProjectTree());
+ 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 }));
+ }
+ } else {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process not found', hideDuration: 2000 }));
+ }
+ };
+
+export const copyProcess = (resource: CopyFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ 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 });
+ dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_COPY_FORM_NAME }));
+ return process;
+ } catch (e) {
+ dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_COPY_FORM_NAME }));
+ throw new Error('Could not copy the process.');
+ }
+ };
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { startSubmit, stopSubmit, initialize } from 'redux-form';
+import { ServiceRepository } from '~/services/services';
+import { RootState } from '~/store/store';
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
+import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
+import { projectPanelActions } from '~/store/project-panel/project-panel-action';
+import { getProcess, getProcessStatus, ProcessStatus } from '~/store/processes/process';
+
+export const PROCESS_MOVE_FORM_NAME = 'processMoveFormName';
+
+export const openMoveProcessDialog = (resource: { name: string, uuid: string }) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const process = getProcess(resource.uuid)(getState().resources);
+ if (process) {
+ const processStatus = getProcessStatus(process);
+ if (processStatus === ProcessStatus.DRAFT) {
+ dispatch<any>(resetPickerProjectTree());
+ 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 }));
+ }
+ } else {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process not found', hideDuration: 2000 }));
+ }
+ };
+
+export const moveProcess = (resource: MoveToFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ 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 });
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+ dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_MOVE_FORM_NAME }));
+ return process;
+ } catch (e) {
+ 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 }));
+ }
+ return;
+ }
+ };
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { initialize, startSubmit, stopSubmit } from 'redux-form';
+import { RootState } from "~/store/store";
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
+import { ServiceRepository } from "~/services/services";
+import { getProcess } from '~/store/processes/process';
+import { projectPanelActions } from '~/store/project-panel/project-panel-action';
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+
+export interface ProcessUpdateFormDialogData {
+ uuid: string;
+ name: string;
+}
+
+export const PROCESS_UPDATE_FORM_NAME = 'processUpdateFormName';
+
+export const openProcessUpdateDialog = (resource: ProcessUpdateFormDialogData) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const process = getProcess(resource.uuid)(getState().resources);
+ if(process) {
+ dispatch(initialize(PROCESS_UPDATE_FORM_NAME, { ...resource, name: process.containerRequest.name }));
+ dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_UPDATE_FORM_NAME, data: {} }));
+ } else {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process not found', hideDuration: 2000 }));
+ }
+ };
+
+export const updateProcess = (resource: ProcessUpdateFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(startSubmit(PROCESS_UPDATE_FORM_NAME));
+ try {
+ const process = await services.containerRequestService.get(resource.uuid);
+ const updatedProcess = await services.containerRequestService.update(resource.uuid, { ...process, name: resource.name });
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+ dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_UPDATE_FORM_NAME }));
+ return updatedProcess;
+ } catch (e) {
+ 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 }));
+ }
+ return;
+ }
+ };
\ No newline at end of file
? getTimeDiff(container.finishedAt || '', container.startedAt || '')
: 0;
-export const getProcessStatusColor = (status: string, { customs }: ArvadosTheme) => {
+export const getProcessStatusColor = (status: string, { customs, palette }: ArvadosTheme) => {
switch (status) {
case ProcessStatus.RUNNING:
return customs.colors.blue500;
case ProcessStatus.FAILED:
return customs.colors.red900;
default:
- return customs.colors.grey500;
+ return palette.grey["500"];
}
};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { unionize, ofType, UnionOf } from "~/common/unionize";
+
+export const progressIndicatorActions = unionize({
+ START_WORKING: ofType<string>(),
+ STOP_WORKING: ofType<string>(),
+ PERSIST_STOP_WORKING: ofType<string>(),
+ TOGGLE_WORKING: ofType<{ id: string, working: boolean }>()
+});
+
+export type ProgressIndicatorAction = UnionOf<typeof progressIndicatorActions>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ProgressIndicatorAction, progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
+
+export type ProgressIndicatorState = { id: string, working: boolean }[];
+
+const initialState: ProgressIndicatorState = [];
+
+export const progressIndicatorReducer = (state: ProgressIndicatorState = initialState, action: ProgressIndicatorAction) => {
+ const startWorking = (id: string) => state.find(p => p.id === id) ? state : state.concat({ id, working: true });
+ const stopWorking = (id: string) => state.filter(p => p.id !== id);
+
+ return progressIndicatorActions.match(action, {
+ START_WORKING: id => startWorking(id),
+ STOP_WORKING: id => stopWorking(id),
+ PERSIST_STOP_WORKING: id => state.map(p => ({
+ ...p,
+ working: p.id === id ? false : p.working
+ })),
+ TOGGLE_WORKING: ({ id, working }) => working ? startWorking(id) : stopWorking(id),
+ default: () => state,
+ });
+};
+
+export function isSystemWorking(state: ProgressIndicatorState): boolean {
+ return state.length > 0 && state.reduce((working, p) => working ? true : p.working, false);
+}
--- /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 { RootState } from '~/store/store';
+
+export type WithProgressStateProps = {
+ working: boolean;
+};
+
+export const withProgress = (id: string) =>
+ (component: React.ComponentType<WithProgressStateProps>) =>
+ connect(mapStateToProps(id))(component);
+
+export const mapStateToProps = (id: string) => (state: RootState): WithProgressStateProps => {
+ const progress = state.progressIndicator.find(p => p.id === id);
+ return { working: progress ? progress.working : false };
+};
import { ProjectResource } from "~/models/project";
import { updateResources } from "~/store/resources/resources-actions";
import { getProperty } from "~/store/properties/properties";
-import { snackbarActions } from '../snackbar/snackbar-actions';
+import { snackbarActions, SnackbarKind } from '../snackbar/snackbar-actions';
+import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions.ts';
import { DataExplorer, getDataExplorer } from '../data-explorer/data-explorer-reducer';
import { ListResults } from '~/services/common-service/common-resource-service';
import { loadContainers } from '../processes/processes-actions';
api.dispatch(projectPanelDataExplorerIsNotSet());
} else {
try {
+ api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
const response = await this.services.groupsService.contents(projectUuid, getParams(dataExplorer));
+ api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
api.dispatch(updateResources(response.items));
await api.dispatch<any>(loadMissingProcessesInformation(response.items));
api.dispatch(setItems(response));
} catch (e) {
+ api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+ api.dispatch(projectPanelActions.SET_ITEMS({
+ items: [],
+ itemsAvailable: 0,
+ page: 0,
+ rowsPerPage: dataExplorer.rowsPerPage
+ }));
api.dispatch(couldNotFetchProjectContents());
}
}
}
};
-const setItems = (listResults: ListResults<GroupContentsResource>) =>
+export const setItems = (listResults: ListResults<GroupContentsResource>) =>
projectPanelActions.SET_ITEMS({
...listResultsToDataExplorerItemsMeta(listResults),
items: listResults.items.map(resource => resource.uuid),
});
-const getParams = (dataExplorer: DataExplorer) => ({
+export const getParams = (dataExplorer: DataExplorer) => ({
...dataExplorerToListParams(dataExplorer),
order: getOrder(dataExplorer),
filters: getFilters(dataExplorer),
});
-const getFilters = (dataExplorer: DataExplorer) => {
+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);
.getFilters();
};
-const getOrder = (dataExplorer: DataExplorer) => {
+export const getOrder = (dataExplorer: DataExplorer) => {
const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
const order = new OrderBuilder<ProjectResource>();
if (sortColumn) {
const couldNotFetchProjectContents = () =>
snackbarActions.OPEN_SNACKBAR({
- message: 'Could not fetch project contents.'
+ message: 'Could not fetch project contents.',
+ kind: SnackbarKind.ERROR
});
const projectPanelDataExplorerIsNotSet = () =>
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { DataExplorerMiddlewareService, listResultsToDataExplorerItemsMeta, dataExplorerToListParams } from '../data-explorer/data-explorer-middleware-service';
+import { ServiceRepository } from '~/services/services';
+import { MiddlewareAPI, Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { getDataExplorer, DataExplorer } from '~/store/data-explorer/data-explorer-reducer';
+import { updateFavorites } from '~/store/favorites/favorites-actions';
+import { updateResources } from '~/store/resources/resources-actions';
+import { loadMissingProcessesInformation } from '~/store/project-panel/project-panel-middleware-service';
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { sharedWithMePanelActions } from './shared-with-me-panel-actions';
+import { ListResults } from '~/services/common-service/common-resource-service';
+import { GroupContentsResource } from '~/services/groups-service/groups-service';
+import { SortDirection } from '~/components/data-table/data-column';
+import { OrderBuilder, OrderDirection } from '~/services/api/order-builder';
+import { ProjectResource } from '~/models/project';
+import { ProjectPanelColumnNames } from '~/views/project-panel/project-panel';
+import { FilterBuilder } from '~/services/api/filter-builder';
+
+export class SharedWithMeMiddlewareService extends DataExplorerMiddlewareService {
+ constructor(private services: ServiceRepository, id: string) {
+ super(id);
+ }
+
+ async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+ const state = api.getState();
+ const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
+ try {
+ const response = await this.services.groupsService.shared(getParams(dataExplorer));
+ api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
+ api.dispatch(updateResources(response.items));
+ await api.dispatch<any>(loadMissingProcessesInformation(response.items));
+ api.dispatch(setItems(response));
+ } catch (e) {
+ api.dispatch(couldNotFetchSharedItems());
+ }
+
+ }
+}
+
+export const getParams = (dataExplorer: DataExplorer) => ({
+ ...dataExplorerToListParams(dataExplorer),
+ order: getOrder(dataExplorer),
+ filters: getFilters(dataExplorer),
+});
+
+export const getFilters = (dataExplorer: DataExplorer) => {
+ const filters = new FilterBuilder()
+ .addILike("name", dataExplorer.searchValue)
+ .getFilters();
+ return `[${filters}]`;
+};
+
+export const getOrder = (dataExplorer: DataExplorer) => {
+ const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+ const order = new OrderBuilder<ProjectResource>();
+ if (sortColumn) {
+ const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
+ ? OrderDirection.ASC
+ : OrderDirection.DESC;
+ const columnName = sortColumn && sortColumn.name === ProjectPanelColumnNames.NAME ? "name" : "createdAt";
+ return order
+ .addOrder(sortDirection, columnName)
+ .getOrder();
+ } else {
+ return order.getOrder();
+ }
+};
+
+export const setItems = (listResults: ListResults<GroupContentsResource>) =>
+ sharedWithMePanelActions.SET_ITEMS({
+ ...listResultsToDataExplorerItemsMeta(listResults),
+ items: listResults.items.map(resource => resource.uuid),
+ });
+
+const couldNotFetchSharedItems = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Could not fetch shared items.'
+ });
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { bindDataExplorerActions } from "../data-explorer/data-explorer-action";
+import { Dispatch } from 'redux';
+import { ServiceRepository } from "~/services/services";
+import { RootState } from '~/store/store';
+
+export const SHARED_WITH_ME_PANEL_ID = "sharedWithMePanel";
+export const sharedWithMePanelActions = bindDataExplorerActions(SHARED_WITH_ME_PANEL_ID);
+
+export const loadSharedWithMePanel = () =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(sharedWithMePanelActions.REQUEST_ITEMS());
+ };
+
+
import { TreeItemStatus } from "~/components/tree/tree";
import { getNodeAncestors, getNodeValue, getNodeAncestorsIds, getNode } from '~/models/tree';
import { ProjectResource } from '~/models/project';
+import { progressIndicatorActions } from '../progress-indicator/progress-indicator-actions';
export enum SidePanelTreeCategory {
PROJECTS = 'Projects',
}
};
-const getSidePanelTreeNode = (nodeId: string) => (treePicker: TreePicker) => {
+export const getSidePanelTreeNode = (nodeId: string) => (treePicker: TreePicker) => {
const sidePanelTree = getTreePicker(SIDE_PANEL_TREE)(treePicker);
return sidePanelTree
? getNodeValue(nodeId)(sidePanelTree)
: undefined;
};
-const getSidePanelTreeNodeAncestorsIds = (nodeId: string) => (treePicker: TreePicker) => {
+export const getSidePanelTreeNodeAncestorsIds = (nodeId: string) => (treePicker: TreePicker) => {
const sidePanelTree = getTreePicker(SIDE_PANEL_TREE)(treePicker);
return sidePanelTree
? getNodeAncestorsIds(nodeId)(sidePanelTree)
import { Dispatch } from 'redux';
import { isSidePanelTreeCategory, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
-import { navigateToFavorites, navigateTo, navigateToTrash } from '../navigation/navigation-action';
+import { navigateToFavorites, navigateTo, navigateToTrash, navigateToSharedWithMe } from '../navigation/navigation-action';
import { snackbarActions } from '~/store/snackbar/snackbar-actions';
export const navigateFromSidePanel = (id: string) =>
return navigateToFavorites;
case SidePanelTreeCategory.TRASH:
return navigateToTrash;
+ case SidePanelTreeCategory.SHARED_WITH_ME:
+ return navigateToSharedWithMe;
default:
return sidePanelTreeCategoryNotAvailable(id);
}
import { unionize, ofType, UnionOf } from "~/common/unionize";
+export interface SnackbarMessage {
+ message: string;
+ hideDuration: number;
+ kind: SnackbarKind;
+}
+
+export enum SnackbarKind {
+ SUCCESS = 1,
+ ERROR = 2,
+ INFO = 3,
+ WARNING = 4
+}
+
export const snackbarActions = unionize({
- OPEN_SNACKBAR: ofType<{message: string; hideDuration?: number}>(),
- CLOSE_SNACKBAR: ofType<{}>()
+ OPEN_SNACKBAR: ofType<{message: string; hideDuration?: number, kind?: SnackbarKind}>(),
+ CLOSE_SNACKBAR: ofType<{}>(),
+ SHIFT_MESSAGES: ofType<{}>()
});
export type SnackbarAction = UnionOf<typeof snackbarActions>;
//
// SPDX-License-Identifier: AGPL-3.0
-import { SnackbarAction, snackbarActions } from "./snackbar-actions";
+import { SnackbarAction, snackbarActions, SnackbarKind, SnackbarMessage } from "./snackbar-actions";
export interface SnackbarState {
- message: string;
+ messages: SnackbarMessage[];
open: boolean;
- hideDuration: number;
}
const DEFAULT_HIDE_DURATION = 3000;
const initialState: SnackbarState = {
- message: "",
- open: false,
- hideDuration: DEFAULT_HIDE_DURATION
+ messages: [],
+ open: false
};
export const snackbarReducer = (state = initialState, action: SnackbarAction) => {
return snackbarActions.match(action, {
- OPEN_SNACKBAR: data => ({ ...initialState, ...data, open: true }),
- CLOSE_SNACKBAR: () => initialState,
+ OPEN_SNACKBAR: data => {
+ return {
+ open: true,
+ messages: state.messages.concat({
+ message: data.message,
+ hideDuration: data.hideDuration ? data.hideDuration : DEFAULT_HIDE_DURATION,
+ kind: data.kind ? data.kind : SnackbarKind.INFO
+ })
+ };
+ },
+ CLOSE_SNACKBAR: () => ({
+ ...state,
+ open: false
+ }),
+ SHIFT_MESSAGES: () => {
+ const messages = state.messages.filter((m, idx) => idx > 0);
+ return {
+ open: messages.length > 0,
+ messages
+ };
+ },
default: () => state,
});
};
import { TRASH_PANEL_ID } from "~/store/trash-panel/trash-panel-action";
import { processLogsPanelReducer } from './process-logs-panel/process-logs-panel-reducer';
import { processPanelReducer } from '~/store/process-panel/process-panel-reducer';
+import { SHARED_WITH_ME_PANEL_ID } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
+import { SharedWithMeMiddlewareService } from './shared-with-me-panel/shared-with-me-middleware-service';
+import { progressIndicatorReducer } from './progress-indicator/progress-indicator-reducer';
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
const trashPanelMiddleware = dataExplorerMiddleware(
new TrashPanelMiddlewareService(services, TRASH_PANEL_ID)
);
+ const sharedWithMePanelMiddleware = dataExplorerMiddleware(
+ new SharedWithMeMiddlewareService(services, SHARED_WITH_ME_PANEL_ID)
+ );
const middlewares: Middleware[] = [
routerMiddleware(history),
thunkMiddleware.withExtraArgument(services),
projectPanelMiddleware,
favoritePanelMiddleware,
- trashPanelMiddleware
+ trashPanelMiddleware,
+ sharedWithMePanelMiddleware,
];
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
return createStore(rootReducer, enhancer);
snackbar: snackbarReducer,
treePicker: treePickerReducer,
fileUploader: fileUploaderReducer,
- processPanel: processPanelReducer
+ processPanel: processPanelReducer,
+ progressIndicator: progressIndicatorReducer
});
import { ProjectResource } from "~/models/project";
import { ProjectPanelColumnNames } from "~/views/project-panel/project-panel";
import { updateFavorites } from "~/store/favorites/favorites-actions";
-import { TrashableResource } from "~/models/resource";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
import { updateResources } from "~/store/resources/resources-actions";
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
}
try {
+ api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
const userUuid = this.services.authService.getUuid()!;
const listResults = await this.services.groupsService
.contents(userUuid, {
.addIsA("uuid", typeFilters.map(f => f.type))
.addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
.addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
+ .addEqual("is_trashed", true)
.getFilters(),
recursive: true,
includeTrash: true
});
+ api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
- const items = listResults.items
- .filter(it => (it as TrashableResource).isTrashed)
- .map(it => it.uuid);
+ const items = listResults.items.map(it => it.uuid);
api.dispatch(trashPanelActions.SET_ITEMS({
...listResultsToDataExplorerItemsMeta(listResults),
api.dispatch<any>(updateFavorites(items));
api.dispatch(updateResources(listResults.items));
} catch (e) {
+ api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+ api.dispatch(trashPanelActions.SET_ITEMS({
+ items: [],
+ itemsAvailable: 0,
+ page: 0,
+ rowsPerPage: dataExplorer.rowsPerPage
+ }));
api.dispatch(couldNotFetchTrashContents());
}
}
const couldNotFetchTrashContents = () =>
snackbarActions.OPEN_SNACKBAR({
- message: 'Could not fetch trash contents.'
+ message: 'Could not fetch trash contents.',
+ kind: SnackbarKind.ERROR
});
+
import { Dispatch } from "redux";
import { RootState } from "~/store/store";
import { ServiceRepository } from "~/services/services";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
import { trashPanelActions } from "~/store/trash-panel/trash-panel-action";
import { activateSidePanelTreeItem, loadSidePanelTreeProjects } from "~/store/side-panel-tree/side-panel-tree-actions";
import { projectPanelActions } from "~/store/project-panel/project-panel-action";
-import { ResourceKind, TrashableResource } from "~/models/resource";
+import { ResourceKind } from "~/models/resource";
export const toggleProjectTrashed = (uuid: string, ownerUuid: string, isTrashed: boolean) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
- if (isTrashed) {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash..." }));
- await services.groupsService.untrash(uuid);
- dispatch<any>(activateSidePanelTreeItem(uuid));
- dispatch(trashPanelActions.REQUEST_ITEMS());
- dispatch(snackbarActions.CLOSE_SNACKBAR());
+ try {
+ if (isTrashed) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash..." }));
+ await services.groupsService.untrash(uuid);
+ dispatch<any>(activateSidePanelTreeItem(uuid));
+ dispatch(trashPanelActions.REQUEST_ITEMS());
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Restored from trash",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS
+ }));
+ } else {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash..." }));
+ await services.groupsService.trash(uuid);
+ dispatch<any>(loadSidePanelTreeProjects(ownerUuid));
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Added to trash",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS
+ }));
+ }
+ } catch (e) {
dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Restored from trash",
- hideDuration: 2000
- }));
- } else {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash..." }));
- await services.groupsService.trash(uuid);
- dispatch<any>(loadSidePanelTreeProjects(ownerUuid));
- dispatch(snackbarActions.CLOSE_SNACKBAR());
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Added to trash",
- hideDuration: 2000
+ message: "Could not move project to trash",
+ kind: SnackbarKind.ERROR
}));
}
};
export const toggleCollectionTrashed = (uuid: string, isTrashed: boolean) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
- if (isTrashed) {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash..." }));
- await services.collectionService.untrash(uuid);
- dispatch(trashPanelActions.REQUEST_ITEMS());
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Restored from trash",
- hideDuration: 2000
- }));
- } else {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash..." }));
- await services.collectionService.trash(uuid);
- dispatch(projectPanelActions.REQUEST_ITEMS());
+ try {
+ if (isTrashed) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash..." }));
+ await services.collectionService.untrash(uuid);
+ dispatch(trashPanelActions.REQUEST_ITEMS());
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Restored from trash",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS
+ }));
+ } else {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash..." }));
+ await services.collectionService.trash(uuid);
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Added to trash",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS
+ }));
+ }
+ } catch (e) {
dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Added to trash",
- hideDuration: 2000
+ message: "Could not move collection to trash",
+ kind: SnackbarKind.ERROR
}));
}
};
import { snackbarActions } from '../snackbar/snackbar-actions';
import { loadFavoritePanel } from '../favorite-panel/favorite-panel-action';
import { openProjectPanel, projectPanelActions } from '~/store/project-panel/project-panel-action';
-import { activateSidePanelTreeItem, initSidePanelTree, SidePanelTreeCategory, loadSidePanelTreeProjects } from '../side-panel-tree/side-panel-tree-actions';
+import { activateSidePanelTreeItem, initSidePanelTree, SidePanelTreeCategory, loadSidePanelTreeProjects, getSidePanelTreeNodeAncestorsIds } from '../side-panel-tree/side-panel-tree-actions';
import { loadResource, updateResources } from '../resources/resources-actions';
import { favoritePanelActions } from '~/store/favorite-panel/favorite-panel-action';
import { projectPanelColumns } from '~/views/project-panel/project-panel';
import { favoritePanelColumns } from '~/views/favorite-panel/favorite-panel';
import { matchRootRoute } from '~/routes/routes';
-import { setCollectionBreadcrumbs, setProjectBreadcrumbs, setSidePanelBreadcrumbs, setProcessBreadcrumbs } from '../breadcrumbs/breadcrumbs-actions';
+import { setCollectionBreadcrumbs, setProjectBreadcrumbs, setSidePanelBreadcrumbs, setProcessBreadcrumbs, setSharedWithMeBreadcrumbs } from '../breadcrumbs/breadcrumbs-actions';
import { navigateToProject } from '../navigation/navigation-action';
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
import { ServiceRepository } from '~/services/services';
import * as collectionUpdateActions from '~/store/collections/collection-update-actions';
import * as collectionMoveActions from '~/store/collections/collection-move-actions';
import * as processesActions from '../processes/processes-actions';
+import * as processMoveActions from '~/store/processes/process-move-actions';
+import * as processUpdateActions from '~/store/processes/process-update-actions';
+import * as processCopyActions from '~/store/processes/process-copy-actions';
import { trashPanelColumns } from "~/views/trash-panel/trash-panel";
import { loadTrashPanel, trashPanelActions } from "~/store/trash-panel/trash-panel-action";
import { initProcessLogsPanel } from '../process-logs-panel/process-logs-panel-actions';
import { loadProcessPanel } from '~/store/process-panel/process-panel-actions';
+import { sharedWithMePanelActions } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
+import { loadSharedWithMePanel } from '../shared-with-me-panel/shared-with-me-panel-actions';
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
export const loadWorkbench = () =>
async (dispatch: Dispatch, getState: () => RootState) => {
dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
+ dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
dispatch<any>(initSidePanelTree());
if (router.location) {
const match = matchRootRoute(router.location.pathname);
};
export const loadProject = (uuid: string) =>
- async (dispatch: Dispatch) => {
- await dispatch<any>(activateSidePanelTreeItem(uuid));
- dispatch<any>(setProjectBreadcrumbs(uuid));
- dispatch<any>(openProjectPanel(uuid));
+ async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(openProjectPanel(uuid));
+ await dispatch(activateSidePanelTreeItem(uuid));
+ dispatch(setProjectBreadcrumbs(uuid));
dispatch(loadDetailsPanel(uuid));
};
}
};
-export const copyCollection = (data: collectionCopyActions.CollectionCopyFormDialogData) =>
+export const copyCollection = (data: CopyFormDialogData) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
try {
const collection = await dispatch<any>(collectionCopyActions.copyCollection(data));
await dispatch<any>(activateSidePanelTreeItem(process.containerRequest.ownerUuid));
dispatch<any>(setProcessBreadcrumbs(uuid));
dispatch(loadDetailsPanel(uuid));
-
+
+ };
+
+export const updateProcess = (data: processUpdateActions.ProcessUpdateFormDialogData) =>
+ async (dispatch: Dispatch) => {
+ try {
+ const process = await dispatch<any>(processUpdateActions.updateProcess(data));
+ if (process) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Process has been successfully updated.",
+ hideDuration: 2000
+ }));
+ dispatch<any>(updateResources([process]));
+ dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
+ }
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
+ }
+ };
+
+export const moveProcess = (data: MoveToFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const process = await dispatch<any>(processMoveActions.moveProcess(data));
+ dispatch<any>(updateResources([process]));
+ dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process has been moved.', hideDuration: 2000 }));
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
+ }
+ };
+
+export const copyProcess = (data: CopyFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const process = await dispatch<any>(processCopyActions.copyProcess(data));
+ dispatch<any>(updateResources([process]));
+ dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process has been copied.', hideDuration: 2000 }));
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
+ }
};
export const loadProcessLog = (uuid: string) =>
dispatch<any>(loadProject(currentProjectPanelUuid));
}
};
+
+export const loadSharedWithMe = (dispatch: Dispatch) => {
+ dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.SHARED_WITH_ME));
+ dispatch<any>(loadSharedWithMePanel());
+ dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.SHARED_WITH_ME));
+};
export const COPY_FILE_VALIDATION = [require];
export const MOVE_TO_VALIDATION = [require];
+
+export const PROCESS_NAME_VALIDATION = [require, maxLength(255)];
\ No newline at end of file
} from "~/components/icon/icon";
import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
import { navigateToProcessLogs } from '~/store/navigation/navigation-action';
+import { openMoveProcessDialog } from '~/store/processes/process-move-actions';
+import { openProcessUpdateDialog } from "~/store/processes/process-update-actions";
+import { openCopyProcessDialog } from '~/store/processes/process-copy-actions';
+import { openProcessCommandDialog } from '../../../store/processes/process-command-actions';
export const processActionSet: ContextMenuActionSet = [[
{
icon: RenameIcon,
name: "Edit process",
- execute: (dispatch, resource) => {
- // add code
- }
+ execute: (dispatch, resource) => dispatch<any>(openProcessUpdateDialog(resource))
},
{
icon: ShareIcon,
{
icon: MoveToIcon,
name: "Move to",
- execute: (dispatch, resource) => {
- // add code
- }
+ execute: (dispatch, resource) => dispatch<any>(openMoveProcessDialog(resource))
},
{
component: ToggleFavoriteAction,
{
icon: CopyIcon,
name: "Copy to project",
- execute: (dispatch, resource) => {
- // add code
- }
+ execute: (dispatch, resource) => dispatch<any>(openCopyProcessDialog(resource))
},
{
icon: ReRunProcessIcon,
icon: CommandIcon,
name: "Command",
execute: (dispatch, resource) => {
- // add code
+ dispatch<any>(openProcessCommandDialog(resource.uuid));
}
},
{
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import { ToggleFavoriteAction } from "../actions/favorite-action";
+import { toggleFavorite } from "~/store/favorites/favorites-actions";
+import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, RemoveIcon } from "~/components/icon/icon";
+import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
+import { openMoveProcessDialog } from '~/store/processes/process-move-actions';
+import { openProcessUpdateDialog } from "~/store/processes/process-update-actions";
+import { openCopyProcessDialog } from '~/store/processes/process-copy-actions';
+
+export const processResourceActionSet: ContextMenuActionSet = [[
+ {
+ icon: RenameIcon,
+ name: "Edit process",
+ execute: (dispatch, resource) => dispatch<any>(openProcessUpdateDialog(resource))
+ },
+ {
+ icon: ShareIcon,
+ name: "Share",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ icon: MoveToIcon,
+ name: "Move to",
+ execute: (dispatch, resource) => dispatch<any>(openMoveProcessDialog(resource))
+ },
+ {
+ component: ToggleFavoriteAction,
+ execute: (dispatch, resource) => {
+ dispatch<any>(toggleFavorite(resource)).then(() => {
+ dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
+ });
+ }
+ },
+ {
+ icon: CopyIcon,
+ name: "Copy to project",
+ execute: (dispatch, resource) => dispatch<any>(openCopyProcessDialog(resource))
+ },
+ {
+ icon: DetailsIcon,
+ name: "View details",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ icon: RemoveIcon,
+ name: "Remove",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ }
+]];
COLLECTION = 'Collection',
COLLECTION_RESOURCE = 'CollectionResource',
PROCESS = "Process",
+ PROCESS_RESOURCE = 'ProcessResource',
PROCESS_LOGS = "ProcessLogs"
}
import * as React from 'react';
import { Dialog, DialogActions, DialogTitle, DialogContent, WithStyles, withStyles, StyleRulesCallback, Button, Typography, Paper } 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 { compose } from 'redux';
+import { connect } from 'react-redux';
+import { CurrentTokenDialogData, getCurrentTokenDialogData } from '~/store/current-token-dialog/current-token-dialog-actions';
+import { RootState } from '~/store/store';
+import { DefaultCodeSnippet } from '~/components/default-code-snippet/default-code-snippet';
type CssRules = 'link' | 'paper' | 'button';
}
});
-interface CurrentTokenDataProps {
- currentToken?: string;
- open: boolean;
-}
-
-interface CurrentTokenActionProps {
- handleClose: () => void;
-}
-
-type CurrentTokenProps = CurrentTokenDataProps & CurrentTokenActionProps & WithStyles<CssRules>;
-
-export const CurrentTokenDialog = withStyles(styles)(
- class extends React.Component<CurrentTokenProps> {
+type CurrentTokenProps = CurrentTokenDialogData & WithDialogProps<{}> & WithStyles<CssRules>;
- render() {
- const { classes, open, handleClose, currentToken } = this.props;
- return (
- <Dialog open={open} onClose={handleClose} fullWidth={true} maxWidth='md'>
- <DialogTitle>Current Token</DialogTitle>
- <DialogContent>
- <Typography variant='body1' paragraph={true}>
- The Arvados API token is a secret key that enables the Arvados SDKs to access Arvados with the proper permissions.
+export const CurrentTokenDialog = compose(
+ withStyles(styles),
+ connect(getCurrentTokenDialogData),
+ withDialog('currentTokenDialog')
+)(class extends React.Component<CurrentTokenProps> {
+ render() {
+ const { classes, open, closeDialog, ...data } = this.props;
+ return <Dialog
+ open={open}
+ onClose={closeDialog}
+ fullWidth={true}
+ maxWidth='md'>
+ <DialogTitle>Current Token</DialogTitle>
+ <DialogContent>
+ <Typography variant='body1' paragraph={true}>
+ The Arvados API token is a secret key that enables the Arvados SDKs to access Arvados with the proper permissions.
<Typography component='p'>
- For more information see
+ For more information see
<a href='http://doc.arvados.org/user/reference/api-tokens.html' target='blank' className={classes.link}>
- Getting an API token.
+ Getting an API token.
</a>
- </Typography>
+ </Typography>
+ </Typography>
+ <Typography variant='body1' paragraph={true}>
+ Paste the following lines at a shell prompt to set up the necessary environment for Arvados SDKs to authenticate to your klingenc account.
</Typography>
-
- <Typography variant='body1' paragraph={true}>
- Paste the following lines at a shell prompt to set up the necessary environment for Arvados SDKs to authenticate to your klingenc account.
- </Typography>
-
- <Paper className={classes.paper} elevation={0}>
- <Typography variant='body1'>
- HISTIGNORE=$HISTIGNORE:'export ARVADOS_API_TOKEN=*'
- </Typography>
- <Typography variant='body1'>
- export ARVADOS_API_TOKEN={currentToken}
- </Typography>
- <Typography variant='body1'>
- export ARVADOS_API_HOST=api.ardev.roche.com
- </Typography>
- <Typography variant='body1'>
- unset ARVADOS_API_HOST_INSECURE
- </Typography>
- </Paper>
- <Typography variant='body1'>
- Arvados
+ <DefaultCodeSnippet lines={[getSnippet(data)]} />
+ <Typography variant='body1'>
+ Arvados
<a href='http://doc.arvados.org/user/reference/api-tokens.html' target='blank' className={classes.link}>virtual machines</a>
- do this for you automatically. This setup is needed only when you use the API remotely (e.g., from your own workstation).
+ do this for you automatically. This setup is needed only when you use the API remotely (e.g., from your own workstation).
</Typography>
- </DialogContent>
- <DialogActions>
- <Button onClick={handleClose} className={classes.button} color="primary">CLOSE</Button>
- </DialogActions>
- </Dialog>
- );
- }
+ </DialogContent>
+ <DialogActions>
+ <Button onClick={closeDialog} className={classes.button} color="primary">CLOSE</Button>
+ </DialogActions>
+ </Dialog>;
}
+}
);
+
+const getSnippet = ({ apiHost, currentToken }: CurrentTokenDialogData) =>
+`HISTIGNORE=$HISTIGNORE:'export ARVADOS_API_TOKEN=*'
+export ARVADOS_API_TOKEN=${currentToken}
+export ARVADOS_API_HOST=${apiHost}
+unset ARVADOS_API_HOST_INSECURE`;
}
const mapStateToProps = (state: RootState, { id }: Props) => {
- return getDataExplorer(state.dataExplorer, id);
+ const progress = state.progressIndicator.find(p => p.id === id);
+ const working = progress && progress.working;
+ return { ...getDataExplorer(state.dataExplorer, id), working };
};
const mapDispatchToProps = () => {
constructor(protected item: T) {}
getTitle(): string {
- return this.item.name;
+ return this.item.name || 'Projects';
}
abstract getIcon(className?: string): React.ReactElement<any>;
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { Drawer, IconButton, Tabs, Tab, Typography, Grid } from '@material-ui/core';
+import { IconButton, Tabs, Tab, Typography, Grid, Tooltip } from '@material-ui/core';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import { Transition } from 'react-transition-group';
import { ArvadosTheme } from '~/common/custom-theme';
import * as classnames from "classnames";
import { connect } from 'react-redux';
import { EmptyDetails } from "./empty-details";
import { DetailsData } from "./details-data";
import { DetailsResource } from "~/models/details";
-import { getResource } from '../../store/resources/resources';
+import { getResource } from '~/store/resources/resources';
-type CssRules = 'drawerPaper' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'headerTitle' | 'tabContainer';
+type CssRules = 'root' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'tabContainer';
-const drawerWidth = 320;
+const DRAWER_WIDTH = 320;
+const SLIDE_TIMEOUT = 500;
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- container: {
+ root: {
+ background: theme.palette.background.paper,
+ borderLeft: `1px solid ${theme.palette.divider}`,
+ height: '100%',
+ overflow: 'hidden',
+ transition: `width ${SLIDE_TIMEOUT}ms ease`,
width: 0,
- position: 'relative',
- height: 'auto',
- transition: 'width 0.5s ease',
- '&$opened': {
- width: drawerWidth
- }
},
- opened: {},
- drawerPaper: {
- position: 'relative',
- width: drawerWidth
+ opened: {
+ width: DRAWER_WIDTH,
+ },
+ container: {
+ maxWidth: 'none',
+ width: DRAWER_WIDTH,
},
headerContainer: {
color: theme.palette.grey["600"],
margin: `${theme.spacing.unit}px 0`,
- textAlign: 'center'
+ textAlign: 'center',
},
headerIcon: {
- fontSize: '2.125rem'
- },
- headerTitle: {
- overflowWrap: 'break-word',
- wordWrap: 'break-word'
+ fontSize: '2.125rem',
},
tabContainer: {
- padding: theme.spacing.unit * 3
- }
+ overflow: 'auto',
+ padding: theme.spacing.unit * 3,
+ },
});
const getItem = (resource: DetailsResource): DetailsData => {
this.setState({ tabsValue: value });
}
- renderTabContainer = (children: React.ReactElement<any>) =>
- <Typography className={this.props.classes.tabContainer} component="div">
- {children}
- </Typography>
-
render() {
- const { classes, onCloseDrawer, isOpened, item } = this.props;
- const { tabsValue } = this.state;
+ const { classes, isOpened } = this.props;
return (
- <Typography component="div"
- className={classnames([classes.container, { [classes.opened]: isOpened }])}>
- <Drawer variant="permanent" anchor="right" classes={{ paper: classes.drawerPaper }}>
- <Typography component="div" className={classes.headerContainer}>
- <Grid container alignItems='center' justify='space-around'>
- <Grid item xs={2}>
- {item.getIcon(classes.headerIcon)}
- </Grid>
- <Grid item xs={8}>
- <Typography variant="title" className={classes.headerTitle}>
- {item.getTitle()}
- </Typography>
- </Grid>
- <Grid item>
- <IconButton color="inherit" onClick={onCloseDrawer}>
- {<CloseIcon />}
- </IconButton>
- </Grid>
- </Grid>
- </Typography>
- <Tabs value={tabsValue} onChange={this.handleChange}>
- <Tab disableRipple label="Details" />
- <Tab disableRipple label="Activity" disabled />
- </Tabs>
- {tabsValue === 0 && this.renderTabContainer(
- <Grid container direction="column">
- {item.getDetails()}
- </Grid>
- )}
- {tabsValue === 1 && this.renderTabContainer(
- <Grid container direction="column" />
- )}
- </Drawer>
- </Typography>
+ <Grid
+ container
+ direction="column"
+ className={classnames([classes.root, { [classes.opened]: isOpened }])}>
+ <Transition
+ in={isOpened}
+ timeout={SLIDE_TIMEOUT}
+ unmountOnExit>
+ {this.renderContent()}
+ </Transition>
+ </Grid>
);
}
+
+ renderContent() {
+ const { classes, onCloseDrawer, item } = this.props;
+ const { tabsValue } = this.state;
+ return <Grid
+ container
+ direction="column"
+ item
+ xs
+ className={classes.container} >
+ <Grid
+ item
+ className={classes.headerContainer}
+ container
+ alignItems='center'
+ justify='space-around'
+ wrap="nowrap">
+ <Grid item xs={2}>
+ {item.getIcon(classes.headerIcon)}
+ </Grid>
+ <Grid item xs={8}>
+ <Tooltip title={item.getTitle()}>
+ <Typography variant="title" noWrap>
+ {item.getTitle()}
+ </Typography>
+ </Tooltip>
+ </Grid>
+ <Grid item>
+ <IconButton color="inherit" onClick={onCloseDrawer}>
+ <CloseIcon />
+ </IconButton>
+ </Grid>
+ </Grid>
+ <Grid item>
+ <Tabs value={tabsValue} onChange={this.handleChange}>
+ <Tab disableRipple label="Details" />
+ <Tab disableRipple label="Activity" disabled />
+ </Tabs>
+ </Grid>
+ <Grid item xs className={this.props.classes.tabContainer} >
+ {tabsValue === 0
+ ? item.getDetails()
+ : null}
+ </Grid>
+ </Grid >;
+ }
}
)
);
{...props}
/>;
-export const CollectionPartialCopyFields = () => <div style={{ display: 'flex' }}>
- <div>
- <CollectionNameField />
- <CollectionDescriptionField />
- </div>
+export const CollectionPartialCopyFields = () => <div>
+ <CollectionNameField />
+ <CollectionDescriptionField />
<CollectionProjectPickerField />
</div>;
import { ProjectTreePickerField } from '~/views-components/project-tree-picker/project-tree-picker';
import { COPY_NAME_VALIDATION, COPY_FILE_VALIDATION } from '~/validators/validators';
import { TextField } from "~/components/text-field/text-field";
-import { CollectionCopyFormDialogData } from "~/store/collections/collection-copy-actions";
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
-type CopyFormDialogProps = WithDialogProps<string> & InjectedFormProps<CollectionCopyFormDialogData>;
+type CopyFormDialogProps = WithDialogProps<string> & InjectedFormProps<CopyFormDialogData>;
-export const DialogCollectionCopy = (props: CopyFormDialogProps) =>
+export const DialogCopy = (props: CopyFormDialogProps) =>
<FormDialog
dialogTitle='Make a copy'
- formFields={CollectionCopyFields}
+ formFields={CopyDialogFields}
submitLabel='Copy'
{...props}
/>;
-const CollectionCopyFields = () => <span>
+const CopyDialogFields = () => <span>
<Field
name='name'
component={TextField}
import { compose } from "redux";
import { withDialog } from "~/store/dialog/with-dialog";
import { reduxForm } from 'redux-form';
-import { COLLECTION_COPY_FORM_NAME, CollectionCopyFormDialogData } from '~/store/collections/collection-copy-actions';
-import { DialogCollectionCopy } from "~/views-components/dialog-copy/dialog-collection-copy";
+import { COLLECTION_COPY_FORM_NAME } from '~/store/collections/collection-copy-actions';
+import { DialogCopy } from "~/views-components/dialog-copy/dialog-copy";
import { copyCollection } from '~/store/workbench/workbench-actions';
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
export const CopyCollectionDialog = compose(
withDialog(COLLECTION_COPY_FORM_NAME),
- reduxForm<CollectionCopyFormDialogData>({
+ reduxForm<CopyFormDialogData>({
form: COLLECTION_COPY_FORM_NAME,
onSubmit: (data, dispatch) => {
dispatch(copyCollection(data));
}
})
-)(DialogCollectionCopy);
\ No newline at end of file
+)(DialogCopy);
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { compose } from "redux";
+import { withDialog } from "~/store/dialog/with-dialog";
+import { reduxForm } from 'redux-form';
+import { PROCESS_COPY_FORM_NAME } from '~/store/processes/process-copy-actions';
+import { DialogCopy } from "~/views-components/dialog-copy/dialog-copy";
+import { copyProcess } from '~/store/workbench/workbench-actions';
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
+
+export const CopyProcessDialog = compose(
+ withDialog(PROCESS_COPY_FORM_NAME),
+ reduxForm<CopyFormDialogData>({
+ form: PROCESS_COPY_FORM_NAME,
+ onSubmit: (data, dispatch) => {
+ dispatch(copyProcess(data));
+ }
+ })
+)(DialogCopy);
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { compose } from 'redux';
+import { withDialog } from "~/store/dialog/with-dialog";
+import { reduxForm } from 'redux-form';
+import { PROCESS_MOVE_FORM_NAME } from '~/store/processes/process-move-actions';
+import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
+import { DialogMoveTo } from '~/views-components/dialog-move/dialog-move-to';
+import { moveProcess } from '~/store/workbench/workbench-actions';
+
+export const MoveProcessDialog = compose(
+ withDialog(PROCESS_MOVE_FORM_NAME),
+ reduxForm<MoveToFormDialogData>({
+ form: PROCESS_MOVE_FORM_NAME,
+ onSubmit: (data, dispatch) => {
+ dispatch(moveProcess(data));
+ }
+ })
+)(DialogMoveTo);
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { compose } from "redux";
+import { reduxForm } from 'redux-form';
+import { withDialog } from "~/store/dialog/with-dialog";
+import { DialogProcessUpdate } from '~/views-components/dialog-update/dialog-process-update';
+import { PROCESS_UPDATE_FORM_NAME, ProcessUpdateFormDialogData } from '~/store/processes/process-update-actions';
+import { updateProcess } from "~/store/workbench/workbench-actions";
+
+export const UpdateProcessDialog = compose(
+ withDialog(PROCESS_UPDATE_FORM_NAME),
+ reduxForm<ProcessUpdateFormDialogData>({
+ form: PROCESS_UPDATE_FORM_NAME,
+ onSubmit: (data, dispatch) => {
+ dispatch(updateProcess(data));
+ }
+ })
+)(DialogProcessUpdate);
\ No newline at end of file
--- /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 { WithDialogProps } from '~/store/dialog/with-dialog';
+import { ProcessUpdateFormDialogData } from '~/store/processes/process-update-actions';
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { ProcessNameField } from '~/views-components/form-fields/process-form-fields';
+
+type DialogProcessProps = WithDialogProps<{}> & InjectedFormProps<ProcessUpdateFormDialogData>;
+
+export const DialogProcessUpdate = (props: DialogProcessProps) =>
+ <FormDialog
+ dialogTitle='Edit Process'
+ formFields={ProcessEditFields}
+ submitLabel='Save'
+ {...props}
+ />;
+
+const ProcessEditFields = () => <span>
+ <ProcessNameField />
+</span>;
validate={COLLECTION_PROJECT_VALIDATION} />;
const ProjectPicker = (props: WrappedFieldProps) =>
- <div style={{ width: '400px', height: '144px', display: 'flex', flexDirection: 'column' }}>
+ <div style={{ height: '144px', display: 'flex', flexDirection: 'column' }}>
<ProjectTreePicker onChange={projectUuid => props.input.onChange(projectUuid)} />
</div>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Field, WrappedFieldProps } from "redux-form";
+import { TextField } from "~/components/text-field/text-field";
+import { PROCESS_NAME_VALIDATION } from "~/validators/validators";
+
+export const ProcessNameField = () =>
+ <Field
+ name='name'
+ component={TextField}
+ validate={PROCESS_NAME_VALIDATION}
+ label="Process Name" />;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { MenuItem } from "@material-ui/core";
+import { User, getUserFullname } from "~/models/user";
+import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
+import { UserPanelIcon } from "~/components/icon/icon";
+import { DispatchProp, connect } from 'react-redux';
+import { logout } from "~/store/auth/auth-action";
+import { RootState } from "~/store/store";
+import { openCurrentTokenDialog } from '../../store/current-token-dialog/current-token-dialog-actions';
+
+interface AccountMenuProps {
+ user?: User;
+}
+
+const mapStateToProps = (state: RootState): AccountMenuProps => ({
+ user: state.auth.user
+});
+
+export const AccountMenu = connect(mapStateToProps)(
+ ({ user, dispatch }: AccountMenuProps & DispatchProp<any>) =>
+ user
+ ? <DropdownMenu
+ icon={<UserPanelIcon />}
+ id="account-menu"
+ title="Account Management">
+ <MenuItem>
+ {getUserFullname(user)}
+ </MenuItem>
+ <MenuItem onClick={() => dispatch(openCurrentTokenDialog)}>Current token</MenuItem>
+ <MenuItem>My account</MenuItem>
+ <MenuItem onClick={() => dispatch(logout())}>Logout</MenuItem>
+ </DropdownMenu>
+ : null);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Button } from '@material-ui/core';
+import { DispatchProp, connect } from 'react-redux';
+import { login } from '~/store/auth/auth-action';
+
+export const AnonymousMenu = connect()(
+ ({ dispatch }: DispatchProp<any>) =>
+ <Button
+ color="inherit"
+ onClick={() => dispatch(login())}>
+ Sign in
+ </Button>);
// SPDX-License-Identifier: AGPL-3.0
import * as React from "react";
-import { MenuItem, Typography } from "@material-ui/core";
+import { MenuItem, Typography, ListSubheader } from "@material-ui/core";
import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
import { ImportContactsIcon, HelpIcon } from "~/components/icon/icon";
import { ArvadosTheme } from '~/common/custom-theme';
icon={<HelpIcon />}
id="help-menu"
title="Help">
- <li className={classes.title}>
- <Typography variant="body1">Help</Typography>
- </li>
+ <MenuItem disabled>Help</MenuItem>
{
links.map(link =>
<MenuItem key={link.title}>
<a href={link.link} target="_blank" className={classes.link}>
- <ImportContactsIcon className={classes.icon} />
- <Typography variant="body1" className={classes.linkTitle}>{link.title}</Typography>
+ <ImportContactsIcon className={classes.icon} />
+ <Typography variant="body1" className={classes.linkTitle}>{link.title}</Typography>
</a>
</MenuItem>
)
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from "react";
-import { mount, configure } from "enzyme";
-import * as Adapter from "enzyme-adapter-react-16";
-import { MainAppBar, MainAppBarProps } from './main-app-bar';
-import { SearchBar } from "~/components/search-bar/search-bar";
-import { Breadcrumbs } from "~/components/breadcrumbs/breadcrumbs";
-import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
-import { Button, MenuItem, IconButton } from "@material-ui/core";
-import { User } from "~/models/user";
-import { MemoryRouter } from 'react-router-dom';
-
-configure({ adapter: new Adapter() });
-
-describe("<MainAppBar />", () => {
-
- const user: User = {
- firstName: "Test",
- lastName: "User",
- email: "test.user@example.com",
- uuid: "",
- ownerUuid: ""
- };
-
- it("renders all components and the menu for authenticated user if user prop has value", () => {
- const mainAppBar = mount(
- <MemoryRouter>
- <MainAppBar
- {...mockMainAppBarProps({ user })}
- />
- </MemoryRouter>
- );
- expect(mainAppBar.find(SearchBar)).toHaveLength(1);
- expect(mainAppBar.find(Breadcrumbs)).toHaveLength(1);
- expect(mainAppBar.find(DropdownMenu)).toHaveLength(2);
- });
-
- it("renders only the menu for anonymous user if user prop is undefined", () => {
- const menuItems = { accountMenu: [], helpMenu: [], anonymousMenu: [{ label: 'Sign in' }] };
- const mainAppBar = mount(
- <MemoryRouter>
- <MainAppBar
- {...mockMainAppBarProps({ user: undefined, menuItems })}
- />
- </MemoryRouter>
- );
- expect(mainAppBar.find(SearchBar)).toHaveLength(0);
- expect(mainAppBar.find(Breadcrumbs)).toHaveLength(0);
- expect(mainAppBar.find(DropdownMenu)).toHaveLength(0);
- expect(mainAppBar.find(Button)).toHaveLength(1);
- });
-
- it("communicates with <SearchBar />", () => {
- const onSearch = jest.fn();
- const mainAppBar = mount(
- <MemoryRouter>
- <MainAppBar
- {...mockMainAppBarProps({ searchText: 'search text', searchDebounce: 2000, onSearch, user })}
- />
- </MemoryRouter>
- );
- const searchBar = mainAppBar.find(SearchBar);
- expect(searchBar.prop("value")).toBe("search text");
- expect(searchBar.prop("debounce")).toBe(2000);
- searchBar.prop("onSearch")("new search text");
- expect(onSearch).toBeCalledWith("new search text");
- });
-
- it("communicates with menu", () => {
- const onMenuItemClick = jest.fn();
- const menuItems = { accountMenu: [{ label: "log out" }], helpMenu: [], anonymousMenu: [] };
- const mainAppBar = mount(
- <MemoryRouter>
- <MainAppBar
- {...mockMainAppBarProps({ menuItems, onMenuItemClick, user })}
- />
- </MemoryRouter>
- );
-
- mainAppBar.find(DropdownMenu).at(0).find(IconButton).simulate("click");
- mainAppBar.find(DropdownMenu).at(0).find(MenuItem).at(1).simulate("click");
- expect(onMenuItemClick).toBeCalledWith(menuItems.accountMenu[0]);
- });
-});
-
-const Breadcrumbs = () => <span>Breadcrumbs</span>;
-
-const mockMainAppBarProps = (props: Partial<MainAppBarProps>): MainAppBarProps => ({
- searchText: '',
- breadcrumbs: Breadcrumbs,
- menuItems: {
- accountMenu: [],
- helpMenu: [],
- anonymousMenu: [],
- },
- buildInfo: '',
- onSearch: jest.fn(),
- onMenuItemClick: jest.fn(),
- onDetailsPanelToggle: jest.fn(),
- ...props,
-});
// SPDX-License-Identifier: AGPL-3.0
import * as React from "react";
-import { AppBar, Toolbar, Typography, Grid, IconButton, Badge, Button, MenuItem, Tooltip } from "@material-ui/core";
+import { AppBar, Toolbar, Typography, Grid } from "@material-ui/core";
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import { ArvadosTheme } from '~/common/custom-theme';
import { Link } from "react-router-dom";
-import { User, getUserFullname } from "~/models/user";
+import { User } from "~/models/user";
import { SearchBar } from "~/components/search-bar/search-bar";
-import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
-import { DetailsIcon, NotificationIcon, UserPanelIcon, HelpIcon } from "~/components/icon/icon";
import { Routes } from '~/routes/routes';
+import { NotificationsMenu } from "~/views-components/main-app-bar/notifications-menu";
+import { AccountMenu } from "~/views-components/main-app-bar/account-menu";
+import { AnonymousMenu } from "~/views-components/main-app-bar/anonymous-menu";
+import { HelpMenu } from './help-menu';
+import { ReactNode } from "react";
-type CssRules = 'link';
+type CssRules = 'toolbar' | 'link';
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+const styles: StyleRulesCallback<CssRules> = () => ({
link: {
textDecoration: 'none',
color: 'inherit'
+ },
+ toolbar: {
+ height: '56px'
}
});
-export interface MainAppBarMenuItem {
- label: string;
-}
-
-export interface MainAppBarMenuItems {
- accountMenu: MainAppBarMenuItem[];
- helpMenu: MainAppBarMenuItem[];
- anonymousMenu: MainAppBarMenuItem[];
-}
-
interface MainAppBarDataProps {
searchText: string;
searchDebounce?: number;
- breadcrumbs: React.ComponentType<any>;
user?: User;
- menuItems: MainAppBarMenuItems;
- buildInfo: string;
+ buildInfo?: string;
+ children?: ReactNode;
}
export interface MainAppBarActionProps {
onSearch: (searchText: string) => void;
- onMenuItemClick: (menuItem: MainAppBarMenuItem) => void;
- onDetailsPanelToggle: () => void;
}
export type MainAppBarProps = MainAppBarDataProps & MainAppBarActionProps & WithStyles<CssRules>;
export const MainAppBar = withStyles(styles)(
(props: MainAppBarProps) => {
- return <AppBar position="static">
- <Toolbar>
+ return <AppBar position="absolute">
+ <Toolbar className={props.classes.toolbar}>
<Grid container justify="space-between">
- <Grid item xs={3}>
- <Typography variant="headline" color="inherit" noWrap>
+ <Grid container item xs={3} direction="column" justify="center">
+ <Typography variant="title" color="inherit" noWrap>
<Link to={Routes.ROOT} className={props.classes.link}>
- Arvados 2
+ arvados workbench
</Link>
</Typography>
- <Typography variant="body1" color="inherit" noWrap >
- {props.buildInfo}
- </Typography>
+ <Typography variant="caption" color="inherit">{props.buildInfo}</Typography>
</Grid>
- <Grid item xs={6} container alignItems="center">
- {
- props.user && <SearchBar
- value={props.searchText}
- onSearch={props.onSearch}
- debounce={props.searchDebounce}
- />
- }
+ <Grid
+ item
+ xs={6}
+ container
+ alignItems="center">
+ {props.user && <SearchBar
+ value={props.searchText}
+ onSearch={props.onSearch}
+ debounce={props.searchDebounce}
+ />}
</Grid>
- <Grid item xs={3} container alignItems="center" justify="flex-end">
- {
- props.user ? renderMenuForUser(props) : renderMenuForAnonymous(props)
- }
+ <Grid
+ item
+ xs={3}
+ container
+ alignItems="center"
+ justify="flex-end"
+ wrap="nowrap">
+ {props.user
+ ? <>
+ <NotificationsMenu />
+ <AccountMenu />
+ <HelpMenu />
+ </>
+ : <AnonymousMenu />}
</Grid>
</Grid>
</Toolbar>
- <Toolbar >
- {props.user && <props.breadcrumbs />}
- {props.user && <IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
- <Tooltip title="Additional Info">
- <DetailsIcon />
- </Tooltip>
- </IconButton>}
- </Toolbar>
+ {props.children}
</AppBar>;
}
);
-
-const renderMenuForUser = ({ user, menuItems, onMenuItemClick }: MainAppBarProps) => {
- return (
- <>
- <IconButton color="inherit">
- <Tooltip title="Notification">
- <Badge badgeContent={3} color="primary">
- <NotificationIcon />
- </Badge>
- </Tooltip>
- </IconButton>
- <DropdownMenu icon={<UserPanelIcon />} id="account-menu" title="Account Management">
- <MenuItem>
- {getUserFullname(user)}
- </MenuItem>
- {renderMenuItems(menuItems.accountMenu, onMenuItemClick)}
- </DropdownMenu>
- <DropdownMenu icon={<HelpIcon />} id="help-menu" title="Help">
- {renderMenuItems(menuItems.helpMenu, onMenuItemClick)}
- </DropdownMenu>
- </>
- );
-};
-
-const renderMenuForAnonymous = ({ onMenuItemClick, menuItems }: MainAppBarProps) => {
- return menuItems.anonymousMenu.map((item, index) => (
- <Button key={index} color="inherit" onClick={() => onMenuItemClick(item)}>
- {item.label}
- </Button>
- ));
-};
-
-const renderMenuItems = (menuItems: MainAppBarMenuItem[], onMenuItemClick: (menuItem: MainAppBarMenuItem) => void) => {
- return menuItems.map((item, index) => (
- <MenuItem key={index} onClick={() => onMenuItemClick(item)}>
- {item.label}
- </MenuItem>
- ));
-};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Badge, MenuItem } from '@material-ui/core';
+import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
+import { NotificationIcon } from '~/components/icon/icon';
+
+export const NotificationsMenu =
+ () =>
+ <DropdownMenu
+ icon={
+ <Badge
+ badgeContent={0}
+ color="primary">
+ <NotificationIcon />
+ </Badge>}
+ id="account-menu"
+ title="Notifications">
+ <MenuItem>You are up to date</MenuItem>
+ </DropdownMenu>;
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Toolbar, IconButton, Tooltip, Grid } from "@material-ui/core";
+import { DetailsIcon } from "~/components/icon/icon";
+import { Breadcrumbs } from "~/views-components/breadcrumbs/breadcrumbs";
+import { detailsPanelActions } from "~/store/details-panel/details-panel-action";
+import { connect } from 'react-redux';
+
+interface MainContentBarProps {
+ onDetailsPanelToggle: () => void;
+}
+
+export const MainContentBar = connect(undefined, {
+ onDetailsPanelToggle: detailsPanelActions.TOGGLE_DETAILS_PANEL
+})((props: MainContentBarProps) =>
+ <Toolbar>
+ <Grid container>
+ <Grid container item xs alignItems="center">
+ <Breadcrumbs />
+ </Grid>
+ <Grid item>
+ <Tooltip title="Additional Info">
+ <IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
+ <DetailsIcon />
+ </IconButton>
+ </Tooltip>
+ </Grid>
+ </Grid>
+ </Toolbar>);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dialog, DialogTitle, DialogActions, Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+import { withDialog } from "~/store/dialog/with-dialog";
+import { PROCESS_COMMAND_DIALOG_NAME } from '~/store/processes/process-command-actions';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { ProcessCommandDialogData } from '~/store/processes/process-command-actions';
+import { DefaultCodeSnippet } from "~/components/default-code-snippet/default-code-snippet";
+import { compose } from 'redux';
+
+type CssRules = 'codeSnippet';
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+ codeSnippet: {
+ marginLeft: theme.spacing.unit * 3,
+ marginRight: theme.spacing.unit * 3,
+ }
+});
+
+export const ProcessCommandDialog = compose(
+ withDialog(PROCESS_COMMAND_DIALOG_NAME),
+ withStyles(styles),
+)(
+ (props: WithDialogProps<ProcessCommandDialogData> & WithStyles<CssRules>) =>
+ <Dialog
+ open={props.open}
+ maxWidth="md"
+ onClose={props.closeDialog}
+ style={{ alignSelf: 'stretch' }}>
+ <DialogTitle>{`Command - ${props.data.processName}`}</DialogTitle>
+ <DefaultCodeSnippet
+ className={props.classes.codeSnippet}
+ lines={[props.data.command]} />
+ <DialogActions>
+ <Button
+ variant='flat'
+ color='primary'
+ onClick={props.closeDialog}>
+ Close
+ </Button>
+ </DialogActions>
+ </Dialog>
+);
\ No newline at end of file
--- /dev/null
+// // Copyright (C) The Arvados Authors. All rights reserved.
+// //
+// // SPDX-License-Identifier: AGPL-3.0
+//
+// import * as React from 'react';
+// import { CircularProgress } from '@material-ui/core';
+// import { withProgress } from '~/store/progress-indicator/with-progress';
+// import { WithProgressStateProps } from '~/store/progress-indicator/with-progress';
+// import { ProgressIndicatorData } from '~/store/progress-indicator/progress-indicator-reducer';
+//
+// export const ContentProgress = withProgress(ProgressIndicatorData.CONTENT_PROGRESS)((props: WithProgressStateProps) =>
+// props.started ? <CircularProgress /> : null
+// );
--- /dev/null
+// // Copyright (C) The Arvados Authors. All rights reserved.
+// //
+// // SPDX-License-Identifier: AGPL-3.0
+//
+// import * as React from 'react';
+// import { CircularProgress } from '@material-ui/core';
+// import { withProgress } from '~/store/progress-indicator/with-progress';
+// import { WithProgressStateProps } from '~/store/progress-indicator/with-progress';
+// import { ProgressIndicatorData } from '~/store/progress-indicator/progress-indicator-reducer';
+//
+// export const SidePanelProgress = withProgress(ProgressIndicatorData.SIDE_PANEL_PROGRESS)((props: WithProgressStateProps) =>
+// props.started ? <span style={{ display: 'flex', justifyContent: 'center', marginTop: "40px" }}><CircularProgress /></span> : null
+// );
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+// import * as React from 'react';
+// import { LinearProgress } from '@material-ui/core';
+// import { withProgress } from '~/store/progress-indicator/with-progress';
+// import { WithProgressStateProps } from '~/store/progress-indicator/with-progress';
+// import { ProgressIndicatorData } from '~/store/progress-indicator/progress-indicator-reducer';
+
+// export const WorkbenchProgress = withProgress(ProgressIndicatorData.WORKBENCH_PROGRESS)(
+// (props: WithProgressStateProps) =>
+// props.started ? <LinearProgress color="secondary" /> : null
+// );
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { connect, DispatchProp } from 'react-redux';
+import { RootState } from '~/store/store';
+import { getProperty } from '~/store/properties/properties';
+import { PROJECT_PANEL_CURRENT_UUID } from '~/store/project-panel/project-panel-action';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { PopoverOrigin } from '@material-ui/core/Popover';
+import { StyleRulesCallback, WithStyles, withStyles, Toolbar, Grid, Button, MenuItem, Menu } from '@material-ui/core';
+import { AddIcon, CollectionIcon, ProcessIcon, ProjectIcon } from '~/components/icon/icon';
+import { openProjectCreateDialog } from '~/store/projects/project-create-actions';
+import { openCollectionCreateDialog } from '~/store/collections/collection-create-actions';
+import { matchProjectRoute } from '~/routes/routes';
+
+type CssRules = 'button' | 'menuItem' | 'icon';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ button: {
+ boxShadow: 'none',
+ padding: '2px 10px 2px 5px',
+ fontSize: '0.75rem'
+ },
+ menuItem: {
+ fontSize: '0.875rem',
+ color: theme.palette.grey["700"]
+ },
+ icon: {
+ marginRight: theme.spacing.unit
+ }
+});
+
+interface SidePanelDataProps {
+ currentItemId: string;
+ buttonVisible: boolean;
+}
+
+interface SidePanelState {
+ anchorEl: any;
+}
+
+type SidePanelProps = SidePanelDataProps & DispatchProp & WithStyles<CssRules>;
+
+const transformOrigin: PopoverOrigin = {
+ vertical: -50,
+ horizontal: 0
+};
+
+const isButtonVisible = ({ router }: RootState) => {
+ const pathname = router.location ? router.location.pathname : '';
+ const match = matchProjectRoute(pathname);
+ return !!match;
+};
+
+export const SidePanelButton = withStyles(styles)(
+ connect((state: RootState) => ({
+ currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
+ buttonVisible: isButtonVisible(state)
+ }))(
+ class extends React.Component<SidePanelProps> {
+
+ state: SidePanelState = {
+ anchorEl: undefined
+ };
+
+ render() {
+ const { classes, buttonVisible } = this.props;
+ const { anchorEl } = this.state;
+ return <Toolbar>
+ {buttonVisible && <Grid container>
+ <Grid container item xs alignItems="center" justify="flex-start">
+ <Button variant="contained" color="primary" size="small" className={classes.button}
+ aria-owns={anchorEl ? 'aside-menu-list' : undefined}
+ aria-haspopup="true"
+ onClick={this.handleOpen}>
+ <AddIcon />
+ New
+ </Button>
+ <Menu
+ id='aside-menu-list'
+ anchorEl={anchorEl}
+ open={Boolean(anchorEl)}
+ onClose={this.handleClose}
+ onClick={this.handleClose}
+ transformOrigin={transformOrigin}>
+ <MenuItem className={classes.menuItem} onClick={this.handleNewCollectionClick}>
+ <CollectionIcon className={classes.icon} /> New collection
+ </MenuItem>
+ <MenuItem className={classes.menuItem}>
+ <ProcessIcon className={classes.icon} /> Run a process
+ </MenuItem>
+ <MenuItem className={classes.menuItem} onClick={this.handleNewProjectClick}>
+ <ProjectIcon className={classes.icon} /> New project
+ </MenuItem>
+ </Menu>
+ </Grid>
+ </Grid> }
+ </Toolbar>;
+ }
+
+ handleNewProjectClick = () => {
+ this.props.dispatch<any>(openProjectCreateDialog(this.props.currentItemId));
+ }
+
+ handleNewCollectionClick = () => {
+ this.props.dispatch<any>(openCollectionCreateDialog(this.props.currentItemId));
+ }
+
+ handleClose = () => {
+ this.setState({ anchorEl: undefined });
+ }
+
+ handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
+ this.setState({ anchorEl: event.currentTarget });
+ }
+ }
+ )
+);
\ No newline at end of file
export interface SidePanelTreeProps {
onItemActivation: (id: string) => void;
+ sidePanelProgress?: boolean;
}
type SidePanelTreeActionProps = Pick<TreePickerProps, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen'>;
import * as React from 'react';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import Drawer from '@material-ui/core/Drawer';
import { ArvadosTheme } from '~/common/custom-theme';
import { SidePanelTree, SidePanelTreeProps } from '~/views-components/side-panel-tree/side-panel-tree';
import { compose, Dispatch } from 'redux';
import { connect } from 'react-redux';
import { navigateFromSidePanel } from '../../store/side-panel/side-panel-action';
+import { Grid } from '@material-ui/core';
+import { SidePanelButton } from '~/views-components/side-panel-button/side-panel-button';
+import { RootState } from '~/store/store';
const DRAWER_WITDH = 240;
-type CssRules = 'drawerPaper' | 'toolbar';
+type CssRules = 'root';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- drawerPaper: {
- position: 'relative',
+ root: {
+ background: theme.palette.background.paper,
+ borderRight: `1px solid ${theme.palette.divider}`,
+ height: '100%',
+ overflowX: 'auto',
width: DRAWER_WITDH,
- display: 'flex',
- flexDirection: 'column',
- paddingTop: 58,
- overflow: 'auto',
- },
- toolbar: theme.mixins.toolbar
+ }
});
const mapDispatchToProps = (dispatch: Dispatch): SidePanelTreeProps => ({
}
});
+const mapStateToProps = (state: RootState) => ({
+});
+
export const SidePanel = compose(
withStyles(styles),
- connect(undefined, mapDispatchToProps)
+ connect(mapStateToProps, mapDispatchToProps)
)(({ classes, ...props }: WithStyles<CssRules> & SidePanelTreeProps) =>
- <Drawer
- variant="permanent"
- classes={{ paper: classes.drawerPaper }}>
- <div className={classes.toolbar} />
+ <Grid item xs>
+ <SidePanelButton />
<SidePanelTree {...props} />
- </Drawer>);
+ </Grid>);
import { RootState } from "~/store/store";
import MaterialSnackbar, { SnackbarProps } from "@material-ui/core/Snackbar";
import { Dispatch } from "redux";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
+import IconButton from '@material-ui/core/IconButton';
+import SnackbarContent from '@material-ui/core/SnackbarContent';
+import WarningIcon from '@material-ui/icons/Warning';
+import CheckCircleIcon from '@material-ui/icons/CheckCircle';
+import ErrorIcon from '@material-ui/icons/Error';
+import InfoIcon from '@material-ui/icons/Info';
+import CloseIcon from '@material-ui/icons/Close';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import { ArvadosTheme } from "~/common/custom-theme";
+import { amber, green } from "@material-ui/core/colors";
+import * as classNames from 'classnames';
-const mapStateToProps = (state: RootState): SnackbarProps => ({
- anchorOrigin: { vertical: "bottom", horizontal: "center" },
- open: state.snackbar.open,
- message: <span>{state.snackbar.message}</span>,
- autoHideDuration: state.snackbar.hideDuration
-});
+const mapStateToProps = (state: RootState): SnackbarProps & ArvadosSnackbarProps => {
+ const messages = state.snackbar.messages;
+ return {
+ anchorOrigin: { vertical: "bottom", horizontal: "right" },
+ open: state.snackbar.open,
+ message: <span>{messages.length > 0 ? messages[0].message : ""}</span>,
+ autoHideDuration: messages.length > 0 ? messages[0].hideDuration : 0,
+ kind: messages.length > 0 ? messages[0].kind : SnackbarKind.INFO
+ };
+};
-const mapDispatchToProps = (dispatch: Dispatch): Pick<SnackbarProps, "onClose"> => ({
+const mapDispatchToProps = (dispatch: Dispatch) => ({
onClose: (event: any, reason: string) => {
if (reason !== "clickaway") {
dispatch(snackbarActions.CLOSE_SNACKBAR());
}
+ },
+ onExited: () => {
+ dispatch(snackbarActions.SHIFT_MESSAGES());
}
});
-export const Snackbar = connect(mapStateToProps, mapDispatchToProps)(MaterialSnackbar);
+const ArvadosSnackbar = (props: any) => <MaterialSnackbar {...props}>
+ <ArvadosSnackbarContent {...props}/>
+</MaterialSnackbar>;
+
+type CssRules = "success" | "error" | "info" | "warning" | "icon" | "iconVariant" | "message";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ success: {
+ backgroundColor: green[600]
+ },
+ error: {
+ backgroundColor: theme.palette.error.dark
+ },
+ info: {
+ backgroundColor: theme.palette.primary.dark
+ },
+ warning: {
+ backgroundColor: amber[700]
+ },
+ icon: {
+ fontSize: 20
+ },
+ iconVariant: {
+ opacity: 0.9,
+ marginRight: theme.spacing.unit
+ },
+ message: {
+ display: 'flex',
+ alignItems: 'center'
+ },
+});
+
+interface ArvadosSnackbarProps {
+ kind: SnackbarKind;
+}
+
+const ArvadosSnackbarContent = (props: SnackbarProps & ArvadosSnackbarProps & WithStyles<CssRules>) => {
+ const { classes, className, message, onClose, kind } = props;
+
+ let Icon = InfoIcon;
+ let cssClass = classes.info;
+
+ switch (kind) {
+ case SnackbarKind.INFO:
+ Icon = InfoIcon;
+ cssClass = classes.info;
+ break;
+ case SnackbarKind.WARNING:
+ Icon = WarningIcon;
+ cssClass = classes.warning;
+ break;
+ case SnackbarKind.SUCCESS:
+ Icon = CheckCircleIcon;
+ cssClass = classes.success;
+ break;
+ case SnackbarKind.ERROR:
+ Icon = ErrorIcon;
+ cssClass = classes.error;
+ break;
+ }
+
+ return (
+ <SnackbarContent
+ className={classNames(cssClass, className)}
+ aria-describedby="client-snackbar"
+ message={
+ <span id="client-snackbar" className={classes.message}>
+ <Icon className={classNames(classes.icon, classes.iconVariant)}/>
+ {message}
+ </span>
+ }
+ action={
+ <IconButton
+ key="close"
+ aria-label="Close"
+ color="inherit"
+ onClick={e => {
+ if (onClose) {
+ onClose(e, '');
+ }
+ }}>
+ <CloseIcon className={classes.icon}/>
+ </IconButton>
+ }
+ />
+ );
+};
+
+export const Snackbar = connect(mapStateToProps, mapDispatchToProps)(
+ withStyles(styles)(ArvadosSnackbar)
+);
<CardHeader
avatar={<CollectionIcon className={classes.iconHeader} />}
action={
- <IconButton
- aria-label="More options"
- onClick={this.handleContextMenu}>
- <Tooltip title="More options">
+ <Tooltip title="More options">
+ <IconButton
+ aria-label="More options"
+ onClick={this.handleContextMenu}>
<MoreOptionsIcon />
- </Tooltip>
- </IconButton>
+ </IconButton>
+ </Tooltip>
}
title={item && item.name}
subheader={item && item.description} />
connect(mapStateToProps, mapDispatchToProps)(
class extends React.Component<FavoritePanelProps> {
render() {
- return this.hasAnyFavorites()
- ? <DataExplorer
- id={FAVORITE_PANEL_ID}
- onRowClick={this.props.onItemClick}
- onRowDoubleClick={this.props.onItemDoubleClick}
- onContextMenu={this.props.onContextMenu}
- contextMenuColumn={true}
- dataTableDefaultView={<DataTableDefaultView icon={FavoriteIcon}/>} />
- : <PanelDefaultView
- icon={FavoriteIcon}
- messages={['Your favorites list is empty.']} />;
- }
-
- hasAnyFavorites = () => {
- return Object
- .keys(this.props.favorites)
- .find(uuid => this.props.favorites[uuid]);
+ return <DataExplorer
+ id={FAVORITE_PANEL_ID}
+ onRowClick={this.props.onItemClick}
+ onRowDoubleClick={this.props.onItemDoubleClick}
+ onContextMenu={this.props.onContextMenu}
+ contextMenuColumn={true}
+ dataTableDefaultView={
+ <DataTableDefaultView
+ icon={FavoriteIcon}
+ messages={['Your favorites list is empty.']}
+ />
+ } />;
}
}
)
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
-import { CodeSnippet, CodeSnippetDataProps } from '~/components/code-snippet/code-snippet';
+import { MuiThemeProvider, createMuiTheme, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
+import { CodeSnippet } from '~/components/code-snippet/code-snippet';
import grey from '@material-ui/core/colors/grey';
+type CssRules = 'codeSnippet';
+
+const styles: StyleRulesCallback<CssRules> = () => ({
+ codeSnippet: {
+ maxHeight: '550px',
+ }
+});
+
const theme = createMuiTheme({
overrides: {
MuiTypography: {
}
});
-type ProcessLogCodeSnippet = CodeSnippetDataProps;
+interface ProcessLogCodeSnippetProps {
+ lines: string[];
+}
-export const ProcessLogCodeSnippet = (props: ProcessLogCodeSnippet) =>
- <MuiThemeProvider theme={theme}>
- <CodeSnippet lines={props.lines} />
- </MuiThemeProvider>;
\ No newline at end of file
+export const ProcessLogCodeSnippet = withStyles(styles)(
+ (props: ProcessLogCodeSnippetProps & WithStyles<CssRules>) =>
+ <MuiThemeProvider theme={theme}>
+ <CodeSnippet lines={props.lines} className={props.classes.codeSnippet} />
+ </MuiThemeProvider>);
\ No newline at end of file
process: Process;
}
-export type ProcessLogMainCardProps = ProcessLogMainCardDataProps & CodeSnippetDataProps & ProcessLogFormDataProps & ProcessLogFormActionProps;
+export interface ProcessLogMainCardActionProps {
+ onContextMenu: (event: React.MouseEvent<any>, process: Process) => void;
+}
+
+export type ProcessLogMainCardProps = ProcessLogMainCardDataProps
+ & ProcessLogMainCardActionProps
+ & CodeSnippetDataProps
+ & ProcessLogFormDataProps
+ & ProcessLogFormActionProps;
export const ProcessLogMainCard = withStyles(styles)(
- ({ classes, process, selectedFilter, filters, onChange, lines }: ProcessLogMainCardProps & WithStyles<CssRules>) =>
+ ({ classes, process, selectedFilter, filters, onChange, lines, onContextMenu }: ProcessLogMainCardProps & WithStyles<CssRules>) =>
<Grid item xs={12}>
<Link to={`/processes/${process.containerRequest.uuid}`} className={classes.backLink}>
<BackIcon className={classes.backIcon} /> Back
<CardHeader
avatar={<ProcessIcon className={classes.iconHeader} />}
action={
- <div>
- <IconButton aria-label="More options">
- <Tooltip title="More options">
- <MoreOptionsIcon />
- </Tooltip>
+ <Tooltip title="More options">
+ <IconButton onClick={event => onContextMenu(event, process)} aria-label="More options">
+ <MoreOptionsIcon />
</IconButton>
- </div>
- }
+ </Tooltip>}
title={
<Tooltip title={process.containerRequest.name} placement="bottom-start">
<Typography noWrap variant="title" className={classes.title}>
{process.containerRequest.name}
</Typography>
- </Tooltip>
- }
+ </Tooltip>}
subheader={process.containerRequest.description} />
<CardContent>
{lines.length > 0
- ? < Grid container spacing={24} alignItems='center'>
- <Grid item xs={6}>
- <ProcessLogForm selectedFilter={selectedFilter} filters={filters} onChange={onChange} />
- </Grid>
- <Grid item xs={6} className={classes.link}>
- <Typography component='div'>
- Go to Log collection
+ ? < Grid
+ container
+ spacing={24}
+ direction='column'>
+ <Grid container item>
+ <Grid item xs={6}>
+ <ProcessLogForm selectedFilter={selectedFilter} filters={filters} onChange={onChange} />
+ </Grid>
+ <Grid item xs={6} className={classes.link}>
+ <Typography component='div'>
+ Go to Log collection
</Typography>
+ </Grid>
</Grid>
- <Grid item xs={12}>
+ <Grid item xs>
<ProcessLogCodeSnippet lines={lines} />
</Grid>
</Grid>
import { DefaultView } from '~/components/default-view/default-view';
import { ProcessIcon } from '~/components/icon/icon';
import { CodeSnippetDataProps } from '~/components/code-snippet/code-snippet';
+import { ProcessLogMainCardActionProps } from './process-log-main-card';
export type ProcessLogPanelRootDataProps = {
process?: Process;
} & ProcessLogFormDataProps & CodeSnippetDataProps;
-export type ProcessLogPanelRootActionProps = {
- onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
-} & ProcessLogFormActionProps;
+export type ProcessLogPanelRootActionProps = ProcessLogMainCardActionProps & ProcessLogFormActionProps;
export type ProcessLogPanelRootProps = ProcessLogPanelRootDataProps & ProcessLogPanelRootActionProps;
//
// SPDX-License-Identifier: AGPL-3.0
-import * as React from 'react';
import { RootState } from '~/store/store';
import { connect } from 'react-redux';
import { getProcess } from '~/store/processes/process';
import { Dispatch } from 'redux';
import { openProcessContextMenu } from '~/store/context-menu/context-menu-actions';
-import { matchProcessLogRoute } from '~/routes/routes';
import { ProcessLogPanelRootDataProps, ProcessLogPanelRootActionProps, ProcessLogPanelRoot } from './process-log-panel-root';
import { getProcessPanelLogs } from '~/store/process-logs-panel/process-logs-panel';
import { setProcessLogsPanelFilter } from '~/store/process-logs-panel/process-logs-panel-actions';
};
const mapDispatchToProps = (dispatch: Dispatch): ProcessLogPanelRootActionProps => ({
- onContextMenu: (event: React.MouseEvent<HTMLElement>) => {
- dispatch<any>(openProcessContextMenu(event));
+ onContextMenu: (event, process) => {
+ dispatch<any>(openProcessContextMenu(event, process));
},
- onChange: (filter: FilterOption) => {
+ onChange: filter => {
dispatch(setProcessLogsPanelFilter(filter.value));
}
});
<Chip label={getProcessStatus(process)}
className={classes.chip}
style={{ backgroundColor: getProcessStatusColor(getProcessStatus(process), theme as ArvadosTheme) }} />
- <IconButton
- aria-label="More options"
- onClick={event => onContextMenu(event)}>
- <Tooltip title="More options">
+ <Tooltip title="More options">
+ <IconButton
+ aria-label="More options"
+ onClick={event => onContextMenu(event)}>
<MoreOptionsIcon />
- </Tooltip>
- </IconButton>
+ </IconButton>
+ </Tooltip>
</div>
}
title={
}
export interface ProcessPanelRootActionProps {
- onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, process: Process) => void;
onToggle: (status: string) => void;
}
export type ProcessPanelRootProps = ProcessPanelRootDataProps & ProcessPanelRootActionProps;
-export const ProcessPanelRoot = (props: ProcessPanelRootProps) =>
- props.process
+export const ProcessPanelRoot = ({process, ...props}: ProcessPanelRootProps) =>
+ process
? <Grid container spacing={16} alignItems="stretch">
<Grid item sm={12} md={7}>
<ProcessInformationCard
- process={props.process}
- onContextMenu={props.onContextMenu} />
+ process={process}
+ onContextMenu={event => props.onContextMenu(event, process)} />
</Grid>
<Grid item sm={12} md={5}>
<SubprocessesCard
};
const mapDispatchToProps = (dispatch: Dispatch): ProcessPanelRootActionProps => ({
- onContextMenu: event => {
- dispatch<any>(openProcessContextMenu(event));
+ onContextMenu: (event, process) => {
+ dispatch<any>(openProcessContextMenu(event, process));
},
onToggle: status => {
dispatch<any>(toggleProcessPanelFilter(status));
<Typography noWrap variant="body2" className={classes.status}>
{getProcessStatus(subprocess)}
</Typography>
- <IconButton
- className={classes.options}
- aria-label="More options"
- onClick={onContextMenu}>
- <Tooltip title="More options">
+ <Tooltip title="More options">
+ <IconButton
+ className={classes.options}
+ aria-label="More options"
+ onClick={onContextMenu}>
<MoreOptionsIcon />
- </Tooltip>
- </IconButton>
+ </IconButton>
+ </Tooltip>
</div>
}
title={
export interface ProcessSubprocessesDataProps {
subprocesses: Array<Process>;
- onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, process: Process) => void;
}
export const ProcessSubprocesses = ({ onContextMenu, subprocesses }: ProcessSubprocessesDataProps) => {
return <Grid container spacing={16}>
{subprocesses.map(subprocess =>
<Grid item xs={12} sm={6} md={4} lg={2} key={subprocess.containerRequest.uuid}>
- <ProcessSubprocessesCard onContextMenu={onContextMenu} subprocess={subprocess} />
+ <ProcessSubprocessesCard
+ onContextMenu={event => onContextMenu(event, subprocess)}
+ subprocess={subprocess} />
</Grid>
)}
</Grid>;
height: '100%'
},
title: {
- color: theme.customs.colors.grey700
+ color: theme.palette.grey["700"]
},
gridFilter: {
height: '20px',
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
import { DispatchProp, connect } from 'react-redux';
import { DataColumns } from '~/components/data-table/data-table';
import { SortDirection } from '~/components/data-table/data-column';
import { ResourceKind, Resource } from '~/models/resource';
import { resourceLabel } from '~/common/labels';
-import { ArvadosTheme } from '~/common/custom-theme';
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 { navigateTo } from '~/store/navigation/navigation-action';
import { getProperty } from '~/store/properties/properties';
import { PROJECT_PANEL_CURRENT_UUID } from '~/store/project-panel/project-panel-action';
-import { openCollectionCreateDialog } from '~/store/collections/collection-create-actions';
-import { openProjectCreateDialog } from '~/store/projects/project-create-actions';
-import { filterResources } from '~/store/resources/resources';
-import { PanelDefaultView } from '~/components/panel-default-view/panel-default-view';
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";
-type CssRules = 'root' | "toolbar" | "button";
+type CssRules = 'root' | "button";
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
width: '100%',
height: '100%'
},
- toolbar: {
- paddingBottom: theme.spacing.unit * 3,
- textAlign: "right"
- },
button: {
marginLeft: theme.spacing.unit
},
render() {
const { classes } = this.props;
return <div className={classes.root}>
- <div className={classes.toolbar}>
- <Button color="primary" onClick={this.handleNewCollectionClick} variant="raised" className={classes.button}>
- New collection
- </Button>
- <Button color="primary" variant="raised" className={classes.button}>
- Run a process
- </Button>
- <Button color="primary" onClick={this.handleNewProjectClick} variant="raised" className={classes.button}>
- New project
- </Button>
- </div>
- {this.hasAnyItems()
- ? <DataExplorer
- id={PROJECT_PANEL_ID}
- onRowClick={this.handleRowClick}
- onRowDoubleClick={this.handleRowDoubleClick}
- onContextMenu={this.handleContextMenu}
- contextMenuColumn={true}
- dataTableDefaultView={<DataTableDefaultView icon={ProjectIcon}/>} />
- : <PanelDefaultView
- icon={ProjectIcon}
- messages={['Your project is empty.', 'Please create a project or create a collection and upload a data.']} />
- }
-
+ <DataExplorer
+ id={PROJECT_PANEL_ID}
+ onRowClick={this.handleRowClick}
+ onRowDoubleClick={this.handleRowDoubleClick}
+ onContextMenu={this.handleContextMenu}
+ contextMenuColumn={true}
+ dataTableDefaultView={
+ <DataTableDefaultView
+ icon={ProjectIcon}
+ messages={[
+ 'Your project is empty.',
+ 'Please create a project or create a collection and upload a data.'
+ ]}/>
+ }/>
</div>;
}
- hasAnyItems = () => {
- const resources = filterResources(this.isCurrentItemChild)(this.props.resources);
- return resources.length > 0;
- }
-
isCurrentItemChild = (resource: Resource) => {
return resource.ownerUuid === this.props.currentItemId;
}
- handleNewProjectClick = () => {
- this.props.dispatch<any>(openProjectCreateDialog(this.props.currentItemId));
- }
-
- handleNewCollectionClick = () => {
- this.props.dispatch<any>(openCollectionCreateDialog(this.props.currentItemId));
- }
-
handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
const menuKind = resourceKindToContextMenuKind(resourceUuid);
const resource = getResource<ProjectResource>(resourceUuid)(this.props.resources);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
+import { connect, DispatchProp } from 'react-redux';
+import { RootState } from '~/store/store';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { ShareMeIcon } from '~/components/icon/icon';
+import { ResourcesState, getResource } from '~/store/resources/resources';
+import { navigateTo } from "~/store/navigation/navigation-action";
+import { loadDetailsPanel } from "~/store/details-panel/details-panel-action";
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+import { SHARED_WITH_ME_PANEL_ID } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
+import { openContextMenu } from '~/store/context-menu/context-menu-actions';
+import { GroupResource } from '~/models/group';
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+
+type CssRules = "toolbar" | "button";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ toolbar: {
+ paddingBottom: theme.spacing.unit * 3,
+ textAlign: "right"
+ },
+ button: {
+ marginLeft: theme.spacing.unit
+ },
+});
+
+interface SharedWithMePanelDataProps {
+ resources: ResourcesState;
+}
+
+type SharedWithMePanelProps = SharedWithMePanelDataProps & DispatchProp & WithStyles<CssRules>;
+
+export const SharedWithMePanel = withStyles(styles)(
+ connect((state: RootState) => ({
+ resources: state.resources
+ }))(
+ class extends React.Component<SharedWithMePanelProps> {
+ render() {
+ return <DataExplorer
+ id={SHARED_WITH_ME_PANEL_ID}
+ onRowClick={this.handleRowClick}
+ onRowDoubleClick={this.handleRowDoubleClick}
+ onContextMenu={this.handleContextMenu}
+ contextMenuColumn={false}
+ dataTableDefaultView={<DataTableDefaultView icon={ShareMeIcon} />} />;
+ }
+
+ handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+ const resource = getResource<GroupResource>(resourceUuid)(this.props.resources);
+ if (resource) {
+ this.props.dispatch<any>(openContextMenu(event, {
+ name: '',
+ uuid: resource.uuid,
+ ownerUuid: resource.ownerUuid,
+ isTrashed: resource.isTrashed,
+ kind: resource.kind,
+ menuKind: ContextMenuKind.PROJECT,
+ }));
+ }
+ }
+
+ handleRowDoubleClick = (uuid: string) => {
+ this.props.dispatch<any>(navigateTo(uuid));
+ }
+
+ handleRowClick = (uuid: string) => {
+ this.props.dispatch(loadDetailsPanel(uuid));
+ }
+ }
+ )
+);
const resource = getResource<TrashableResource>(props.uuid)(state.resources);
return { resource, dispatch: props.dispatch };
})((props: { resource?: TrashableResource, dispatch?: Dispatch<any> }) =>
- <IconButton onClick={() => {
- if (props.resource && props.dispatch) {
- props.dispatch(toggleTrashed(
- props.resource.kind,
- props.resource.uuid,
- props.resource.ownerUuid,
- props.resource.isTrashed
- ));
- }
- }}>
- <Tooltip title="Restore">
+ <Tooltip title="Restore">
+ <IconButton onClick={() => {
+ if (props.resource && props.dispatch) {
+ props.dispatch(toggleTrashed(
+ props.resource.kind,
+ props.resource.uuid,
+ props.resource.ownerUuid,
+ props.resource.isTrashed
+ ));
+ }
+ }}>
<RestoreFromTrashIcon />
- </Tooltip>
- </IconButton>
+ </IconButton>
+ </Tooltip>
);
export const trashPanelColumns: DataColumns<string, TrashPanelFilter> = [
selected: true,
type: ResourceKind.COLLECTION
},
- {
- name: resourceLabel(ResourceKind.PROCESS),
- selected: true,
- type: ResourceKind.PROCESS
- },
{
name: resourceLabel(ResourceKind.PROJECT),
selected: true,
}))(
class extends React.Component<TrashPanelProps> {
render() {
- return this.hasAnyTrashedResources()
- ? <DataExplorer
- id={TRASH_PANEL_ID}
- onRowClick={this.handleRowClick}
- onRowDoubleClick={this.handleRowDoubleClick}
- onContextMenu={this.handleContextMenu}
- contextMenuColumn={false}
- dataTableDefaultView={<DataTableDefaultView icon={TrashIcon} />} />
- : <PanelDefaultView
- icon={TrashIcon}
- messages={['Your trash list is empty.']} />;
- }
-
- hasAnyTrashedResources = () => {
- // TODO: implement check if there is anything in the trash,
- // without taking pagination into the account
- return true;
+ return <DataExplorer
+ id={TRASH_PANEL_ID}
+ onRowClick={this.handleRowClick}
+ onRowDoubleClick={this.handleRowDoubleClick}
+ onContextMenu={this.handleContextMenu}
+ contextMenuColumn={false}
+ dataTableDefaultView={
+ <DataTableDefaultView
+ icon={TrashIcon}
+ messages={['Your trash list is empty.']}/>
+ } />;
}
handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
import { connect, DispatchProp } from "react-redux";
import { Route, Switch } from "react-router";
-import { login, logout } from "~/store/auth/auth-action";
import { User } from "~/models/user";
import { RootState } from "~/store/store";
-import { MainAppBar, MainAppBarActionProps, MainAppBarMenuItem } from '~/views-components/main-app-bar/main-app-bar';
+import { MainAppBar } from '~/views-components/main-app-bar/main-app-bar';
import { push } from 'react-router-redux';
import { ProjectPanel } from "~/views/project-panel/project-panel";
import { DetailsPanel } from '~/views-components/details-panel/details-panel';
import { SidePanel } from '~/views-components/side-panel/side-panel';
import { ProcessPanel } from '~/views/process-panel/process-panel';
import { ProcessLogPanel } from '~/views/process-log-panel/process-log-panel';
-import { Breadcrumbs } from '~/views-components/breadcrumbs/breadcrumbs';
import { CreateProjectDialog } from '~/views-components/dialog-forms/create-project-dialog';
import { CreateCollectionDialog } from '~/views-components/dialog-forms/create-collection-dialog';
import { CopyCollectionDialog } from '~/views-components/dialog-forms/copy-collection-dialog';
+import { CopyProcessDialog } from '~/views-components/dialog-forms/copy-process-dialog';
import { UpdateCollectionDialog } from '~/views-components/dialog-forms/update-collection-dialog';
+import { UpdateProcessDialog } from '~/views-components/dialog-forms/update-process-dialog';
import { UpdateProjectDialog } from '~/views-components/dialog-forms/update-project-dialog';
+import { MoveProcessDialog } from '~/views-components/dialog-forms/move-process-dialog';
import { MoveProjectDialog } from '~/views-components/dialog-forms/move-project-dialog';
import { MoveCollectionDialog } from '~/views-components/dialog-forms/move-collection-dialog';
import { FilesUploadCollectionDialog } from '~/views-components/dialog-forms/files-upload-collection-dialog';
import { PartialCopyCollectionDialog } from '~/views-components/dialog-forms/partial-copy-collection-dialog';
-
import { TrashPanel } from "~/views/trash-panel/trash-panel";
+import { MainContentBar } from '~/views-components/main-content-bar/main-content-bar';
+import { Grid, LinearProgress } from '@material-ui/core';
+import { SharedWithMePanel } from '../shared-with-me-panel/shared-with-me-panel';
+import SplitterLayout from 'react-splitter-layout';
+import { ProcessCommandDialog } from '~/views-components/process-command-dialog/process-command-dialog';
+import { isSystemWorking } from "~/store/progress-indicator/progress-indicator-reducer";
-const APP_BAR_HEIGHT = 100;
-
-type CssRules = 'root' | 'appBar' | 'content' | 'contentWrapper';
+type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content' | 'appBar';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
- flexGrow: 1,
- zIndex: 1,
overflow: 'hidden',
- position: 'relative',
- display: 'flex',
width: '100vw',
- height: '100vh'
+ height: '100vh',
+ paddingTop: theme.spacing.unit * 8
},
- appBar: {
- zIndex: theme.zIndex.drawer + 1,
- position: "absolute",
- width: "100%"
+ container: {
+ position: 'relative'
+ },
+ splitter: {
+ '& > .layout-splitter': {
+ width: '2px'
+ }
+ },
+ asidePanel: {
+ height: '100%',
+ background: theme.palette.background.default
},
contentWrapper: {
- backgroundColor: theme.palette.background.default,
- display: "flex",
- flexGrow: 1,
+ background: theme.palette.background.default,
minWidth: 0,
- paddingTop: APP_BAR_HEIGHT
},
content: {
- padding: `${theme.spacing.unit}px ${theme.spacing.unit * 3}px`,
- overflowY: "auto",
- flexGrow: 1,
- position: 'relative'
+ minWidth: 0,
+ paddingLeft: theme.spacing.unit * 3,
+ paddingRight: theme.spacing.unit * 3,
},
+ appBar: {
+ zIndex: 1,
+ }
});
interface WorkbenchDataProps {
user?: User;
currentToken?: string;
+ working: boolean;
}
interface WorkbenchGeneralProps {
buildInfo: string;
}
-interface WorkbenchActionProps {
-}
-
-type WorkbenchProps = WorkbenchDataProps & WorkbenchGeneralProps & WorkbenchActionProps & DispatchProp<any> & WithStyles<CssRules>;
-
-interface NavMenuItem extends MainAppBarMenuItem {
- action: () => void;
-}
+type WorkbenchProps = WorkbenchDataProps & WorkbenchGeneralProps & DispatchProp<any> & WithStyles<CssRules>;
interface WorkbenchState {
- isCurrentTokenDialogOpen: boolean;
- anchorEl: any;
searchText: string;
- menuItems: {
- accountMenu: NavMenuItem[],
- helpMenu: NavMenuItem[],
- anonymousMenu: NavMenuItem[]
- };
}
export const Workbench = withStyles(styles)(
(state: RootState) => ({
user: state.auth.user,
currentToken: state.auth.apiToken,
+ working: isSystemWorking(state.progressIndicator)
})
)(
class extends React.Component<WorkbenchProps, WorkbenchState> {
state = {
- isCurrentTokenDialogOpen: false,
- anchorEl: null,
searchText: "",
- breadcrumbs: [],
- menuItems: {
- accountMenu: [
- {
- label: 'Current token',
- action: () => this.toggleCurrentTokenModal()
- },
- {
- label: "Logout",
- action: () => this.props.dispatch(logout())
- },
- {
- label: "My account",
- action: () => this.props.dispatch(push("/my-account"))
- }
- ],
- helpMenu: [
- {
- label: "Help",
- action: () => this.props.dispatch(push("/help"))
- }
- ],
- anonymousMenu: [
- {
- label: "Sign in",
- action: () => this.props.dispatch(login())
- }
- ]
- }
};
-
render() {
- const { classes, user } = this.props;
- return (
- <div className={classes.root}>
- <div className={classes.appBar}>
- <MainAppBar
- breadcrumbs={Breadcrumbs}
- searchText={this.state.searchText}
- user={this.props.user}
- menuItems={this.state.menuItems}
- buildInfo={this.props.buildInfo}
- {...this.mainAppBarActions} />
- </div>
- {user && <SidePanel />}
- <main className={classes.contentWrapper}>
- <div className={classes.content}>
- <Switch>
- <Route path={Routes.PROJECTS} component={ProjectPanel} />
- <Route path={Routes.COLLECTIONS} component={CollectionPanel} />
- <Route path={Routes.FAVORITES} component={FavoritePanel} />
- <Route path={Routes.PROCESSES} component={ProcessPanel} />
- <Route path={Routes.TRASH} component={TrashPanel} />
- <Route path={Routes.PROCESS_LOGS} component={ProcessLogPanel} />
- </Switch>
- </div>
- {user && <DetailsPanel />}
- </main>
- <ContextMenu />
- <Snackbar />
- <CreateProjectDialog />
- <CreateCollectionDialog />
- <RenameFileDialog />
- <PartialCopyCollectionDialog />
- <FileRemoveDialog />
- <CopyCollectionDialog />
- <FileRemoveDialog />
- <MultipleFilesRemoveDialog />
- <UpdateCollectionDialog />
- <FilesUploadCollectionDialog />
- <UpdateProjectDialog />
- <MoveCollectionDialog />
- <MoveProjectDialog />
- <CurrentTokenDialog
- currentToken={this.props.currentToken}
- open={this.state.isCurrentTokenDialogOpen}
- handleClose={this.toggleCurrentTokenModal} />
- </div>
- );
+ const { classes } = this.props;
+ return <>
+ <MainAppBar
+ searchText={this.state.searchText}
+ user={this.props.user}
+ onSearch={this.onSearch}
+ buildInfo={this.props.buildInfo}>
+ {this.props.working ? <LinearProgress color="secondary" /> : null}
+ </MainAppBar>
+ <Grid container direction="column" className={classes.root}>
+ {this.props.user &&
+ <Grid container item xs alignItems="stretch" wrap="nowrap">
+ <Grid container item className={classes.container}>
+ <SplitterLayout customClassName={classes.splitter} percentage={true}
+ primaryIndex={0} primaryMinSize={20} secondaryInitialSize={80} secondaryMinSize={40}>
+ <Grid container item xs component='aside' direction='column' className={classes.asidePanel}>
+ <SidePanel />
+ </Grid>
+ <Grid container item xs component="main" direction="column" className={classes.contentWrapper}>
+ <Grid item>
+ <MainContentBar />
+ </Grid>
+ <Grid item xs className={classes.content}>
+ <Switch>
+ <Route path={Routes.PROJECTS} component={ProjectPanel} />
+ <Route path={Routes.COLLECTIONS} component={CollectionPanel} />
+ <Route path={Routes.FAVORITES} component={FavoritePanel} />
+ <Route path={Routes.PROCESSES} component={ProcessPanel} />
+ <Route path={Routes.TRASH} component={TrashPanel} />
+ <Route path={Routes.PROCESS_LOGS} component={ProcessLogPanel} />
+ <Route path={Routes.SHARED_WITH_ME} component={SharedWithMePanel} />
+ </Switch>
+ </Grid>
+ </Grid>
+ </SplitterLayout>
+ </Grid>
+ <Grid item>
+ <DetailsPanel />
+ </Grid>
+ </Grid>
+ }
+ </Grid>
+ <ContextMenu />
+ <CopyCollectionDialog />
+ <CopyProcessDialog />
+ <CreateCollectionDialog />
+ <CreateProjectDialog />
+ <CurrentTokenDialog />
+ <FileRemoveDialog />
+ <FileRemoveDialog />
+ <FilesUploadCollectionDialog />
+ <MoveCollectionDialog />
+ <MoveProcessDialog />
+ <MoveProjectDialog />
+ <MultipleFilesRemoveDialog />
+ <PartialCopyCollectionDialog />
+ <ProcessCommandDialog />
+ <RenameFileDialog />
+ <Snackbar />
+ <UpdateCollectionDialog />
+ <UpdateProcessDialog />
+ <UpdateProjectDialog />
+ </>;
}
- mainAppBarActions: MainAppBarActionProps = {
- onSearch: searchText => {
- this.setState({ searchText });
- this.props.dispatch(push(`/search?q=${searchText}`));
- },
- onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action(),
- onDetailsPanelToggle: () => {
- this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
- },
- };
+ onSearch = (searchText: string) => {
+ this.setState({ searchText });
+ this.props.dispatch(push(`/search?q=${searchText}`));
+ }
- toggleCurrentTokenModal = () => {
- this.setState({ isCurrentTokenDialogOpen: !this.state.isCurrentTokenDialogOpen });
+ toggleDetailsPanel = () => {
+ this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
}
+
}
)
);
"no-shadowed-variable": false,
"semicolon": true,
"array-type": false,
- "interface-over-type-literal": false
+ "interface-over-type-literal": false,
+ "no-empty": false
},
"linterOptions": {
"exclude": [
import<T = any>(module: string): Promise<T>
}
declare var System: System;
+
+declare module 'react-splitter-layout';
\ No newline at end of file
dependencies:
"@types/enzyme" "*"
-"@types/enzyme@*":
- version "3.1.12"
- resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.12.tgz#293bb07c1ef5932d37add3879e72e0f5bc614f3c"
- dependencies:
- "@types/cheerio" "*"
- "@types/react" "*"
-
-"@types/enzyme@3.1.13":
+"@types/enzyme@*", "@types/enzyme@3.1.13":
version "3.1.13"
resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.13.tgz#4bbc5c81fa40c9fc7efee25c4a23cb37119a33ea"
dependencies:
version "4.14.116"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.116.tgz#5ccf215653e3e8c786a58390751033a9adca0eb9"
-"@types/node@*":
- version "10.5.2"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.2.tgz#f19f05314d5421fe37e74153254201a7bf00a707"
-
-"@types/node@10.7.1":
+"@types/node@*", "@types/node@10.7.1":
version "10.7.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.7.1.tgz#b704d7c259aa40ee052eec678758a68d07132a2e"
"@types/react" "*"
redux "^3.6.0 || ^4.0.0"
+"@types/uuid@3.4.4":
+ version "3.4.4"
+ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.4.tgz#7af69360fa65ef0decb41fd150bf4ca5c0cefdf5"
+ dependencies:
+ "@types/node" "*"
+
abab@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
dependencies:
color-name "1.1.1"
-color-name@1.1.1:
+color-name@1.1.1, color-name@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.1.tgz#4b1415304cf50028ea81643643bd82ea05803689"
-color-name@^1.0.0:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
-
color-string@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991"
dependencies:
domelementtype "1"
-domutils@1.5.1:
+domutils@1.5.1, domutils@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
dependencies:
dom-serializer "0"
domelementtype "1"
-domutils@^1.5.1:
- version "1.7.0"
- resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
- dependencies:
- dom-serializer "0"
- domelementtype "1"
-
dot-prop@^4.1.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
schema-utils "^0.3.0"
webpack-sources "^1.0.1"
-extsprintf@1.3.0:
+extsprintf@1.3.0, extsprintf@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
-extsprintf@^1.2.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
-
fast-deep-equal@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
dependencies:
brace-expansion "^1.1.7"
-minimist@0.0.8:
+minimist@0.0.8, minimist@~0.0.1:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
-minimist@~0.0.1:
- version "0.0.10"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
-
minipass@^2.2.1, minipass@^2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233"
dependencies:
mimic-fn "^1.0.0"
-opn@5.2.0:
+opn@5.2.0, opn@^5.1.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/opn/-/opn-5.2.0.tgz#71fdf934d6827d676cecbea1531f95d354641225"
dependencies:
is-wsl "^1.1.0"
-opn@^5.1.0:
- version "5.3.0"
- resolved "https://registry.yarnpkg.com/opn/-/opn-5.3.0.tgz#64871565c863875f052cfdf53d3e3cb5adb53b1c"
- dependencies:
- is-wsl "^1.1.0"
-
optimist@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
version "1.5.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
-qs@6.5.1:
+qs@6.5.1, qs@~6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
-qs@~6.5.1:
- version "6.5.2"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
-
query-string@^4.1.0:
version "4.3.4"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
prop-types "^15.6.0"
warning "^4.0.1"
-react-is@^16.4.1:
- version "16.4.1"
- resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e"
-
-react-is@^16.4.2:
+react-is@^16.4.1, react-is@^16.4.2:
version "16.4.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.2.tgz#84891b56c2b6d9efdee577cc83501dfc5ecead88"
optionalDependencies:
fsevents "^1.1.3"
+react-splitter-layout@3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/react-splitter-layout/-/react-splitter-layout-3.0.1.tgz#c2e00e69b35d240ab7a44f395d41803c5f4b70ef"
+
react-test-renderer@^16.0.0-0:
version "16.4.1"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.4.1.tgz#f2fb30c2c7b517db6e5b10ed20bb6b0a7ccd8d70"
prop-types "^15.6.0"
react-is "^16.4.1"
-react-transition-group@^2.2.1:
+react-transition-group@2.4.0, react-transition-group@^2.2.1:
version "2.4.0"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.4.0.tgz#1d9391fabfd82e016f26fabd1eec329dbd922b5a"
dependencies:
version "1.1.7"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
-resolve@1.6.0:
+resolve@1.6.0, resolve@^1.1.7, resolve@^1.3.2:
version "1.6.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.6.0.tgz#0fbd21278b27b4004481c395349e7aba60a9ff5c"
dependencies:
path-parse "^1.0.5"
-resolve@^1.1.7, resolve@^1.3.2:
- version "1.8.1"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26"
- dependencies:
- path-parse "^1.0.5"
-
restore-cursor@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
define-property "^0.2.5"
object-copy "^0.1.0"
-"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2":
- version "1.5.0"
- resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
-
-statuses@~1.4.0:
+"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2", statuses@~1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
+uuid@3.3.2, uuid@^3.1.0:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
+
uuid@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
-uuid@^3.1.0:
- version "3.3.2"
- resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
-
validate-npm-package-license@^3.0.1:
version "3.0.3"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338"
dependencies:
iconv-lite "0.4.19"
-whatwg-fetch@2.0.3:
+whatwg-fetch@2.0.3, whatwg-fetch@>=0.10.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
-whatwg-fetch@>=0.10.0:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"
-
whatwg-mimetype@^2.0.0, whatwg-mimetype@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz#f0f21d76cbba72362eb609dbed2a30cd17fcc7d4"
version "0.1.0"
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
-wordwrap@0.0.2:
+wordwrap@0.0.2, wordwrap@~0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
-wordwrap@~0.0.2:
- version "0.0.3"
- resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
-
wordwrap@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"