REACT_APP_ARVADOS_CONFIG_URL=/config.json
REACT_APP_ARVADOS_API_HOST=qr1hi.arvadosapi.com
-REACT_APP_ARVADOS_KEEP_WEB_HOST=collections.qr1hi.arvadosapi.com
HTTPS=true
\ No newline at end of file
"version": "0.1.0",
"private": true,
"dependencies": {
- "@material-ui/core": "1.4.2",
- "@material-ui/icons": "2.0.0",
+ "@material-ui/core": "1.5.0",
+ "@material-ui/icons": "2.0.2",
"@types/lodash": "4.14.116",
- "@types/react-copy-to-clipboard": "4.2.5",
- "@types/react-dropzone": "4.2.1",
- "@types/redux-form": "7.4.4",
+ "@types/react-copy-to-clipboard": "4.2.6",
+ "@types/react-dropzone": "4.2.2",
+ "@types/redux-form": "7.4.5",
"axios": "0.18.0",
"classnames": "2.2.6",
"lodash": "4.17.10",
"react": "16.4.2",
"react-copy-to-clipboard": "5.0.1",
"react-dom": "16.4.2",
- "react-dropzone": "4.2.13",
+ "react-dropzone": "5.0.1",
"react-redux": "5.0.7",
"react-router": "4.3.1",
"react-router-dom": "4.3.1",
"unionize": "2.1.2"
},
"scripts": {
- "start": "REACT_APP_BUILD_NUMBER=$BUILD_NUMBER REACT_APP_GIT_COMMIT=$GIT_COMMIT react-scripts-ts start",
+ "start": "react-scripts-ts start",
"build": "REACT_APP_BUILD_NUMBER=$BUILD_NUMBER REACT_APP_GIT_COMMIT=$GIT_COMMIT react-scripts-ts build",
"test": "react-scripts-ts test --env=jsdom",
"eject": "react-scripts-ts eject",
},
"devDependencies": {
"@types/classnames": "^2.2.4",
- "@types/enzyme": "3.1.12",
- "@types/enzyme-adapter-react-16": "1.0.2",
+ "@types/enzyme": "3.1.13",
+ "@types/enzyme-adapter-react-16": "1.0.3",
"@types/jest": "23.3.1",
- "@types/node": "10.5.5",
+ "@types/node": "10.7.1",
"@types/react": "16.4",
- "@types/react-dom": "16.0.6",
+ "@types/react-dom": "16.0.7",
"@types/react-redux": "6.0.6",
"@types/react-router": "4.0.29",
"@types/react-router-dom": "4.3.0",
"@types/react-router-redux": "5.0.15",
"@types/redux-devtools": "3.0.44",
- "@types/redux-form": "7.4.4",
+ "@types/redux-form": "7.4.5",
"axios-mock-adapter": "1.15.0",
- "enzyme": "3.3.0",
- "enzyme-adapter-react-16": "1.1.1",
+ "enzyme": "3.4.4",
+ "enzyme-adapter-react-16": "1.2.0",
"jest-localstorage-mock": "2.2.0",
"redux-devtools": "3.4.1",
"redux-form": "7.4.2",
errorToken: string;
}
+export enum CommonResourceServiceError {
+ UNIQUE_VIOLATION = 'UniqueViolation',
+ OWNERSHIP_CYCLE = 'OwnershipCycle',
+ UNKNOWN = 'Unknown',
+ NONE = 'None'
+}
+
export class CommonResourceService<T extends Resource> {
static mapResponseKeys = (response: any): Promise<any> =>
}));
}
- update(uuid: string, data: any) {
+ update(uuid: string, data: Partial<T>) {
return CommonResourceService.defaultResponse(
this.serverApi
- .put<T>(this.resourceType + uuid, data));
+ .put<T>(this.resourceType + uuid, data && CommonResourceService.mapKeys(_.snakeCase)(data)));
}
}
+export const getCommonResourceServiceError = (errorResponse: any) => {
+ if ('errors' in errorResponse && 'errorToken' in errorResponse) {
+ const error = errorResponse.errors.join('');
+ switch (true) {
+ case /UniqueViolation/.test(error):
+ return CommonResourceServiceError.UNIQUE_VIOLATION;
+ case /ownership cycle/.test(error):
+ return CommonResourceServiceError.OWNERSHIP_CYCLE;
+ default:
+ return CommonResourceServiceError.UNKNOWN;
+ }
+ }
+ return CommonResourceServiceError.NONE;
+};
+
+
export const CONFIG_URL = process.env.REACT_APP_ARVADOS_CONFIG_URL || "/config.json";
export interface Config {
- apiHost: string;
- keepWebHost: string;
+ auth: {};
+ basePath: string;
+ baseUrl: string;
+ batchPath: string;
+ blobSignatureTtl: number;
+ crunchLimitLogBytesPerJob: number;
+ crunchLogBytesPerEvent: number;
+ crunchLogPartialLineThrottlePeriod: number;
+ crunchLogSecondsBetweenEvents: number;
+ crunchLogThrottleBytes: number;
+ crunchLogThrottleLines: number;
+ crunchLogThrottlePeriod: number;
+ defaultCollectionReplication: number;
+ defaultTrashLifetime: number;
+ description: string;
+ discoveryVersion: string;
+ dockerImageFormats: string[];
+ documentationLink: string;
+ generatedAt: string;
+ gitUrl: string;
+ id: string;
+ keepWebServiceUrl: string;
+ kind: string;
+ maxRequestSize: number;
+ name: string;
+ packageVersion: string;
+ parameters: {};
+ protocol: string;
+ remoteHosts: string;
+ remoteHostsViaDNS: boolean;
+ resources: {};
+ revision: string;
+ rootUrl: string;
+ schemas: {};
+ servicePath: string;
+ sourceVersion: string;
+ source_version: string;
+ title: string;
+ uuidPrefix: string;
+ version: string;
+ websocketUrl: string;
+ workbenchUrl: string;
}
export const fetchConfig = () => {
.get<ConfigJSON>(CONFIG_URL + "?nocache=" + (new Date()).getTime())
.then(response => response.data)
.catch(() => Promise.resolve(getDefaultConfig()))
- .then(mapConfig);
+ .then(config => Axios.get<Config>(getDiscoveryURL(config.API_HOST)))
+ .then(response => response.data);
};
+export const mockConfig = (config: Partial<Config>): Config => ({
+ auth: {},
+ basePath: '',
+ baseUrl: '',
+ batchPath: '',
+ blobSignatureTtl: 0,
+ crunchLimitLogBytesPerJob: 0,
+ crunchLogBytesPerEvent: 0,
+ crunchLogPartialLineThrottlePeriod: 0,
+ crunchLogSecondsBetweenEvents: 0,
+ crunchLogThrottleBytes: 0,
+ crunchLogThrottleLines: 0,
+ crunchLogThrottlePeriod: 0,
+ defaultCollectionReplication: 0,
+ defaultTrashLifetime: 0,
+ description: '',
+ discoveryVersion: '',
+ dockerImageFormats: [],
+ documentationLink: '',
+ generatedAt: '',
+ gitUrl: '',
+ id: '',
+ keepWebServiceUrl: '',
+ kind: '',
+ maxRequestSize: 0,
+ name: '',
+ packageVersion: '',
+ parameters: {},
+ protocol: '',
+ remoteHosts: '',
+ remoteHostsViaDNS: false,
+ resources: {},
+ revision: '',
+ rootUrl: '',
+ schemas: {},
+ servicePath: '',
+ sourceVersion: '',
+ source_version: '',
+ title: '',
+ uuidPrefix: '',
+ version: '',
+ websocketUrl: '',
+ workbenchUrl: '',
+ ...config
+});
+
interface ConfigJSON {
API_HOST: string;
- KEEP_WEB_HOST: string;
}
-const mapConfig = (config: ConfigJSON): Config => ({
- apiHost: addProtocol(config.API_HOST),
- keepWebHost: addProtocol(config.KEEP_WEB_HOST)
-});
-
const getDefaultConfig = (): ConfigJSON => ({
API_HOST: process.env.REACT_APP_ARVADOS_API_HOST || "",
- KEEP_WEB_HOST: process.env.REACT_APP_ARVADOS_KEEP_WEB_HOST || ""
});
-const addProtocol = (url: string) => `${window.location.protocol}//${url}`;
+const getDiscoveryURL = (apiHost: string) => `${window.location.protocol}//${apiHost}/discovery/v1/apis/arvados/v1/rest`;
}
},
MuiInput: {
+ root: {
+ fontSize: '0.875rem'
+ },
underline: {
'&:after': {
borderBottomColor: purple800
}
},
MuiFormLabel: {
+ root: {
+ fontSize: '0.875rem'
+ },
focused: {
"&$focused:not($error)": {
color: purple800
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export const fileToArrayBuffer = (file: File) =>
+ new Promise<ArrayBuffer>((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ resolve(reader.result as ArrayBuffer);
+ };
+ reader.onerror = () => {
+ reject();
+ };
+ reader.readAsArrayBuffer(file);
+ });
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export * from 'unionize';
+import { unionize as originalUnionize, SingleValueRec } from 'unionize';
+
+export function unionize<Record extends SingleValueRec>(record: Record) {
+ return originalUnionize(record, {
+ tag: 'type',
+ value: 'payload'
+ });
+}
+
it('PUT', async () => {
const { open, send, load, progress, createRequest } = mockCreateRequest();
- const onProgress = jest.fn();
const webdav = new WebDAV(undefined, createRequest);
- const promise = webdav.put('foo', 'Test data', { onProgress });
+ const promise = webdav.put('foo', 'Test data');
progress();
load();
const request = await promise;
expect(open).toHaveBeenCalledWith('PUT', 'foo');
expect(send).toHaveBeenCalledWith('Test data');
- expect(onProgress).toHaveBeenCalled();
expect(request).toBeInstanceOf(XMLHttpRequest);
});
.keys(headers)
.forEach(key => r.setRequestHeader(key, headers[key]));
- if (config.onProgress) {
- r.addEventListener('progress', config.onProgress);
+ if (config.onUploadProgress) {
+ r.upload.addEventListener('progress', config.onUploadProgress);
}
r.addEventListener('load', () => resolve(r));
headers?: {
[key: string]: string;
};
- onProgress?: (event: ProgressEvent) => void;
+ onUploadProgress?: (event: ProgressEvent) => void;
}
interface WebDAVDefaults {
url: string;
headers?: { [key: string]: string };
data?: any;
- onProgress?: (event: ProgressEvent) => void;
+ onUploadProgress?: (event: ProgressEvent) => void;
}
}
});
-interface BreadcrumbsProps {
+export interface BreadcrumbsProps {
items: Breadcrumb[];
onClick: (breadcrumb: Breadcrumb) => void;
onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: Breadcrumb) => void;
import { FileTree } from '../file-tree/file-tree';
import { IconButton, Grid, Typography, StyleRulesCallback, withStyles, WithStyles, CardHeader, Card, Button } from '@material-ui/core';
import { CustomizeTableIcon } from '../icon/icon';
-import { connect, DispatchProp } from "react-redux";
-import { Dispatch } from "redux";
-import { RootState } from "~/store/store";
-import { ServiceRepository } from "~/services/services";
export interface CollectionPanelFilesProps {
items: Array<TreeItem<FileTreeData>>;
}
});
-const renameFile = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- services.collectionFilesService.renameTest();
-};
-
-
export const CollectionPanelFiles =
- connect()(
withStyles(styles)(
- ({ onItemMenuOpen, onOptionsMenuOpen, classes, dispatch, ...treeProps }: CollectionPanelFilesProps & DispatchProp & WithStyles<CssRules>) =>
- <Card className={classes.root}>
- <CardHeader
- title="Files"
- action={
- <Button onClick={
- () => {
- dispatch<any>(renameFile());
- }}
- variant='raised'
- color='primary'
- size='small'>
- Upload data
+ ({ onItemMenuOpen, onOptionsMenuOpen, onUploadDataClick, classes, ...treeProps }: CollectionPanelFilesProps & WithStyles<CssRules>) =>
+ <Card className={classes.root}>
+ <CardHeader
+ title="Files"
+ action={
+ <Button onClick={onUploadDataClick}
+ variant='raised'
+ color='primary'
+ size='small'>
+ Upload data
</Button>
- } />
- <CardHeader
- className={classes.cardSubheader}
- action={
- <IconButton onClick={onOptionsMenuOpen}>
- <CustomizeTableIcon />
- </IconButton>
- } />
- <Grid container justify="space-between">
- <Typography variant="caption" className={classes.nameHeader}>
- Name
+ } />
+ <CardHeader
+ className={classes.cardSubheader}
+ action={
+ <IconButton onClick={onOptionsMenuOpen}>
+ <CustomizeTableIcon />
+ </IconButton>
+ } />
+ <Grid container justify="space-between">
+ <Typography variant="caption" className={classes.nameHeader}>
+ Name
</Typography>
- <Typography variant="caption" className={classes.fileSizeHeader}>
- File size
+ <Typography variant="caption" className={classes.fileSizeHeader}>
+ File size
</Typography>
- </Grid>
- <FileTree onMenuOpen={onItemMenuOpen} {...treeProps} />
- </Card>)
-);
+ </Grid>
+ <FileTree onMenuOpen={onItemMenuOpen} {...treeProps} />
+ </Card>);
import { TablePagination } from "@material-ui/core";
import { ProjectIcon } from '../icon/icon';
import { DefaultView } from '../default-view/default-view';
+import { SortDirection } from '../data-table/data-column';
configure({ adapter: new Adapter() });
it("communicates with <SearchInput/>", () => {
const onSearch = jest.fn();
+ const onSetColumns = jest.fn();
const dataExplorer = mount(<DataExplorer
{...mockDataExplorerProps()}
items={[{ name: "item 1" }]}
searchValue="search value"
- onSearch={onSearch} />);
+ onSearch={onSearch}
+ onSetColumns={onSetColumns} />);
expect(dataExplorer.find(SearchInput).prop("value")).toEqual("search value");
dataExplorer.find(SearchInput).prop("onSearch")("new value");
expect(onSearch).toHaveBeenCalledWith("new value");
it("communicates with <ColumnSelector/>", () => {
const onColumnToggle = jest.fn();
- const columns = [{ name: "Column 1", render: jest.fn(), selected: true, configurable: true }];
+ const onSetColumns = jest.fn();
+ const columns = [{ name: "Column 1", render: jest.fn(), selected: true, configurable: true, sortDirection: SortDirection.ASC, filters: [] }];
const dataExplorer = mount(<DataExplorer
{...mockDataExplorerProps()}
columns={columns}
onColumnToggle={onColumnToggle}
- items={[{ name: "item 1" }]} />);
+ items={[{ name: "item 1" }]}
+ onSetColumns={onSetColumns} />);
expect(dataExplorer.find(ColumnSelector).prop("columns")).toBe(columns);
dataExplorer.find(ColumnSelector).prop("onColumnToggle")("columns");
expect(onColumnToggle).toHaveBeenCalledWith("columns");
const onFiltersChange = jest.fn();
const onSortToggle = jest.fn();
const onRowClick = jest.fn();
- const columns = [{ name: "Column 1", render: jest.fn(), selected: true, configurable: true }];
+ const onSetColumns = jest.fn();
+ const columns = [{ name: "Column 1", render: jest.fn(), selected: true, configurable: true, sortDirection: SortDirection.ASC, filters: [] }];
const items = [{ name: "item 1" }];
const dataExplorer = mount(<DataExplorer
{...mockDataExplorerProps()}
items={items}
onFiltersChange={onFiltersChange}
onSortToggle={onSortToggle}
- onRowClick={onRowClick} />);
+ onRowClick={onRowClick}
+ onSetColumns={onSetColumns} />);
expect(dataExplorer.find(DataTable).prop("columns").slice(0, -1)).toEqual(columns);
expect(dataExplorer.find(DataTable).prop("items")).toBe(items);
dataExplorer.find(DataTable).prop("onRowClick")("event", "rowClick");
const dataExplorer = mount(<DataExplorer
{...mockDataExplorerProps()}
items={[]}
- />);
+ onSetColumns={jest.fn()} />);
expect(dataExplorer.find(DataTable)).toHaveLength(0);
expect(dataExplorer.find(DefaultView)).toHaveLength(1);
});
it("communicates with <TablePagination/>", () => {
const onChangePage = jest.fn();
const onChangeRowsPerPage = jest.fn();
+ const onSetColumns = jest.fn();
const dataExplorer = mount(<DataExplorer
{...mockDataExplorerProps()}
items={[{ name: "item 1" }]}
rowsPerPage={50}
onChangePage={onChangePage}
onChangeRowsPerPage={onChangeRowsPerPage}
- />);
+ onSetColumns={onSetColumns} />);
expect(dataExplorer.find(TablePagination).prop("page")).toEqual(10);
expect(dataExplorer.find(TablePagination).prop("rowsPerPage")).toEqual(50);
dataExplorer.find(TablePagination).prop("onChangePage")(undefined, 6);
onChangeRowsPerPage: jest.fn(),
onContextMenu: jest.fn(),
defaultIcon: ProjectIcon,
+ onSetColumns: jest.fn(),
defaultMessages: ['testing'],
});
//
// SPDX-License-Identifier: AGPL-3.0
-import { DataTableFilterItem } from "../data-table-filters/data-table-filters";
import * as React from "react";
+import { DataTableFilterItem } from "../data-table-filters/data-table-filters";
export interface DataColumn<T, F extends DataTableFilterItem = DataTableFilterItem> {
key?: React.Key;
export const resetSortDirection = <T>(column: DataColumn<T>): DataColumn<T> => {
return column.sortDirection ? { ...column, sortDirection: SortDirection.NONE } : column;
};
+
+export const createDataColumn = <T, F extends DataTableFilterItem>(dataColumn: Partial<DataColumn<T, F>>): DataColumn<T, F> => ({
+ key: '',
+ name: '',
+ selected: true,
+ configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: [],
+ render: () => React.createElement('span'),
+ ...dataColumn,
+});
import * as Adapter from "enzyme-adapter-react-16";
import { DataTable, DataColumns } from "./data-table";
import { DataTableFilters } from "../data-table-filters/data-table-filters";
-import { SortDirection } from "./data-column";
+import { SortDirection, createDataColumn } from "./data-column";
configure({ adapter: new Adapter() });
describe("<DataTable />", () => {
it("shows only selected columns", () => {
const columns: DataColumns<string> = [
- {
+ createDataColumn({
name: "Column 1",
render: () => <span />,
selected: true,
configurable: true
- },
- {
+ }),
+ createDataColumn({
name: "Column 2",
render: () => <span />,
selected: true,
configurable: true
- },
- {
+ }),
+ createDataColumn({
name: "Column 3",
render: () => <span />,
selected: false,
configurable: true
- }
+ }),
];
const dataTable = mount(<DataTable
columns={columns}
it("renders column name", () => {
const columns: DataColumns<string> = [
- {
+ createDataColumn({
name: "Column 1",
render: () => <span />,
selected: true,
configurable: true
- }
+ }),
];
const dataTable = mount(<DataTable
columns={columns}
it("uses renderHeader instead of name prop", () => {
const columns: DataColumns<string> = [
- {
+ createDataColumn({
name: "Column 1",
renderHeader: () => <span>Column Header</span>,
render: () => <span />,
selected: true,
configurable: true
- }
+ }),
];
const dataTable = mount(<DataTable
columns={columns}
it("passes column key prop to corresponding cells", () => {
const columns: DataColumns<string> = [
- {
+ createDataColumn({
name: "Column 1",
key: "column-1-key",
render: () => <span />,
selected: true,
configurable: true
- }
+ })
];
const dataTable = mount(<DataTable
columns={columns}
it("renders items", () => {
const columns: DataColumns<string> = [
- {
+ createDataColumn({
name: "Column 1",
render: (item) => <Typography>{item}</Typography>,
selected: true,
configurable: true
- },
- {
+ }),
+ createDataColumn({
name: "Column 2",
render: (item) => <Button>{item}</Button>,
selected: true,
configurable: true
- }
+ })
];
const dataTable = mount(<DataTable
columns={columns}
});
it("passes sorting props to <TableSortLabel />", () => {
- const columns: DataColumns<string> = [{
+ const columns: DataColumns<string> = [
+ createDataColumn({
name: "Column 1",
sortDirection: SortDirection.ASC,
selected: true,
configurable: true,
render: (item) => <Typography>{item}</Typography>
- }];
+ })];
const onSortToggle = jest.fn();
const dataTable = mount(<DataTable
columns={columns}
expect(onSortToggle).toHaveBeenCalledWith(columns[0]);
});
+ it("does not display <DataTableFilter /> if there is no filters provided", () => {
+ const columns: DataColumns<string> = [{
+ name: "Column 1",
+ sortDirection: SortDirection.ASC,
+ selected: true,
+ configurable: true,
+ filters: [],
+ render: (item) => <Typography>{item}</Typography>
+ }];
+ const onFiltersChange = jest.fn();
+ const dataTable = mount(<DataTable
+ columns={columns}
+ items={[]}
+ onFiltersChange={onFiltersChange}
+ onRowClick={jest.fn()}
+ onRowDoubleClick={jest.fn()}
+ onSortToggle={jest.fn()}
+ onContextMenu={jest.fn()} />);
+ expect(dataTable.find(DataTableFilters)).toHaveLength(0);
+ });
+
it("passes filter props to <DataTableFilter />", () => {
const columns: DataColumns<string> = [{
name: "Column 1",
import { ArvadosTheme } from '~/common/custom-theme';
import * as classnames from "classnames";
-type CssRules = 'attribute' | 'label' | 'value' | 'link';
+type CssRules = 'attribute' | 'label' | 'value' | 'lowercaseValue' | 'link';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
attribute: {
marginBottom: theme.spacing.unit
},
label: {
+ boxSizing: 'border-box',
color: theme.palette.grey["500"],
width: '40%'
},
value: {
+ boxSizing: 'border-box',
width: '60%',
display: 'flex',
alignItems: 'flex-start',
textTransform: 'capitalize'
},
+ lowercaseValue: {
+ textTransform: 'lowercase'
+ },
link: {
width: '60%',
color: theme.palette.primary.main,
textDecoration: 'none',
- overflowWrap: 'break-word'
+ overflowWrap: 'break-word',
+ cursor: 'pointer'
}
});
interface DetailsAttributeDataProps {
label: string;
classLabel?: string;
- value?: string | number;
+ value?: React.ReactNode;
classValue?: string;
+ lowercaseValue?: boolean;
link?: string;
children?: React.ReactNode;
}
type DetailsAttributeProps = DetailsAttributeDataProps & WithStyles<CssRules>;
export const DetailsAttribute = withStyles(styles)(
- ({ label, link, value, children, classes, classLabel, classValue }: DetailsAttributeProps) =>
+ ({ label, link, value, children, classes, classLabel, classValue, lowercaseValue }: DetailsAttributeProps) =>
<Typography component="div" className={classes.attribute}>
<Typography component="span" className={classnames([classes.label, classLabel])}>{label}</Typography>
{ link
? <a href={link} className={classes.link} target='_blank'>{value}</a>
- : <Typography component="span" className={classnames([classes.value, classValue])}>
+ : <Typography component="span" className={classnames([classes.value, classValue, { [classes.lowercaseValue]: lowercaseValue }])}>
{value}
{children}
</Typography> }
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { FileUpload } from "~/components/file-upload/file-upload";
+import { Dialog, DialogTitle, DialogContent, DialogActions } from '@material-ui/core/';
+import { Button, CircularProgress } from '@material-ui/core';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { UploadFile } from '~/store/file-uploader/file-uploader-actions';
+
+export interface FilesUploadDialogProps {
+ files: UploadFile[];
+ uploading: boolean;
+ onSubmit: () => void;
+ onChange: (files: File[]) => void;
+}
+
+export const FilesUploadDialog = (props: FilesUploadDialogProps & WithDialogProps<{}>) =>
+ <Dialog open={props.open}
+ disableBackdropClick={true}
+ disableEscapeKeyDown={true}
+ fullWidth={true}
+ maxWidth='sm'>
+ <DialogTitle>Upload data</DialogTitle>
+ <DialogContent>
+ <FileUpload
+ files={props.files}
+ disabled={props.uploading}
+ onDrop={props.onChange}
+ />
+ </DialogContent>
+ <DialogActions>
+ <Button
+ variant='flat'
+ color='primary'
+ disabled={props.uploading}
+ onClick={props.closeDialog}>
+ Cancel
+ </Button>
+ <Button
+ variant='contained'
+ color='primary'
+ type='submit'
+ onClick={props.onSubmit}
+ disabled={props.uploading}>
+ {props.uploading
+ ? <CircularProgress size={20} />
+ : 'Upload data'}
+ </Button>
+ </DialogActions>
+ </Dialog>;
import Dropzone from 'react-dropzone';
import { CloudUploadIcon } from "../icon/icon";
import { formatFileSize, formatProgress, formatUploadSpeed } from "~/common/formatters";
-import { UploadFile } from "~/store/collections/uploader/collection-uploader-actions";
+import { UploadFile } from '~/store/file-uploader/file-uploader-actions';
type CssRules = "root" | "dropzone" | "container" | "uploadIcon";
}
});
-interface FileUploadProps {
+export interface FileUploadProps {
files: UploadFile[];
disabled: boolean;
onDrop: (files: File[]) => void;
export const FileUpload = withStyles(styles)(
({ classes, files, disabled, onDrop }: FileUploadProps & WithStyles<CssRules>) =>
- <Grid container direction={"column"}>
- <Typography variant={"subheading"}>
- Upload data
- </Typography>
<Dropzone className={classes.dropzone} onDrop={files => onDrop(files)} disabled={disabled}>
{files.length === 0 &&
- <Grid container justify="center" alignItems="center" className={classes.container}>
- <Grid item component={"span"}>
- <Typography variant={"subheading"}>
- <CloudUploadIcon className={classes.uploadIcon}/> Drag and drop data or click to browse
+ <Grid container justify="center" alignItems="center" className={classes.container}>
+ <Grid item component={"span"}>
+ <Typography variant={"subheading"}>
+ <CloudUploadIcon className={classes.uploadIcon} /> Drag and drop data or click to browse
</Typography>
- </Grid>
- </Grid>}
+ </Grid>
+ </Grid>}
{files.length > 0 &&
- <Table style={{width: "100%"}}>
+ <Table style={{ width: "100%" }}>
<TableHead>
<TableRow>
<TableCell>File name</TableCell>
</TableRow>
</TableHead>
<TableBody>
- {files.map(f =>
- <TableRow key={f.id}>
- <TableCell>{f.file.name}</TableCell>
- <TableCell>{formatFileSize(f.file.size)}</TableCell>
- <TableCell>{formatUploadSpeed(f.prevLoaded, f.loaded, f.prevTime, f.currentTime)}</TableCell>
- <TableCell>{formatProgress(f.loaded, f.total)}</TableCell>
- </TableRow>
- )}
+ {files.map(f =>
+ <TableRow key={f.id}>
+ <TableCell>{f.file.name}</TableCell>
+ <TableCell>{formatFileSize(f.file.size)}</TableCell>
+ <TableCell>{formatUploadSpeed(f.prevLoaded, f.loaded, f.prevTime, f.currentTime)}</TableCell>
+ <TableCell>{formatProgress(f.loaded, f.total)}</TableCell>
+ </TableRow>
+ )}
</TableBody>
</Table>
}
</Dropzone>
- </Grid>
);
--- /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 { Dialog, DialogActions, DialogContent, DialogTitle } from '@material-ui/core/';
+import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+
+type CssRules = "button" | "lastButton" | "formContainer" | "dialogTitle" | "progressIndicator" | "dialogActions";
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+ button: {
+ marginLeft: theme.spacing.unit
+ },
+ lastButton: {
+ marginLeft: theme.spacing.unit,
+ marginRight: "20px",
+ },
+ formContainer: {
+ display: "flex",
+ flexDirection: "column",
+ marginTop: "20px",
+ },
+ dialogTitle: {
+ paddingBottom: "0"
+ },
+ progressIndicator: {
+ position: "absolute",
+ minWidth: "20px",
+ },
+ dialogActions: {
+ marginBottom: "24px"
+ }
+});
+
+interface DialogProjectProps {
+ cancelLabel?: string;
+ dialogTitle: string;
+ formFields: React.ComponentType<InjectedFormProps<any> & WithDialogProps<any>>;
+ submitLabel?: string;
+}
+
+export const FormDialog = withStyles(styles)((props: DialogProjectProps & WithDialogProps<{}> & InjectedFormProps<any> & WithStyles<CssRules>) =>
+ <Dialog
+ open={props.open}
+ onClose={props.closeDialog}
+ disableBackdropClick={props.submitting}
+ disableEscapeKeyDown={props.submitting}
+ fullWidth
+ maxWidth='sm'>
+ <form>
+ <DialogTitle className={props.classes.dialogTitle}>
+ {props.dialogTitle}
+ </DialogTitle>
+ <DialogContent className={props.classes.formContainer}>
+ <props.formFields {...props} />
+ </DialogContent>
+ <DialogActions className={props.classes.dialogActions}>
+ <Button
+ onClick={props.closeDialog}
+ className={props.classes.button}
+ color="primary"
+ disabled={props.submitting}>
+ {props.cancelLabel || 'Cancel'}
+ </Button>
+ <Button
+ onClick={props.handleSubmit}
+ className={props.classes.lastButton}
+ color="primary"
+ disabled={props.invalid || props.submitting || props.pristine}
+ variant="contained">
+ {props.submitLabel || 'Submit'}
+ {props.submitting && <CircularProgress size={20} className={props.classes.progressIndicator} />}
+ </Button>
+ </DialogActions>
+ </form>
+ </Dialog>
+);
+
+
alignItems: 'center'
},
listItemText: {
- fontWeight: 700
+ fontWeight: 400
},
active: {
color: theme.palette.primary.main,
+++ /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/styles';
-import { ArvadosTheme } from '~/common/custom-theme';
-import { List, ListItem, ListItemIcon, Collapse } from "@material-ui/core";
-import { SidePanelRightArrowIcon, IconType } from '../icon/icon';
-import * as classnames from "classnames";
-import { ListItemTextIcon } from '../list-item-text-icon/list-item-text-icon';
-import { Dispatch } from "redux";
-import { RouteComponentProps, withRouter } from "react-router";
-
-type CssRules = 'active' | 'row' | 'root' | 'list' | 'iconClose' | 'iconOpen' | 'toggableIconContainer' | 'toggableIcon';
-
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- root: {
- overflowY: 'auto',
- minWidth: '240px',
- whiteSpace: 'nowrap',
- marginTop: '52px',
- display: 'flex',
- flexGrow: 1,
- },
- list: {
- padding: '5px 0px 5px 14px',
- minWidth: '240px',
- },
- row: {
- display: 'flex',
- alignItems: 'center',
- },
- toggableIconContainer: {
- color: theme.palette.grey["700"],
- height: '14px',
- width: '14px'
- },
- toggableIcon: {
- fontSize: '14px'
- },
- active: {
- color: theme.palette.primary.main,
- },
- iconClose: {
- transition: 'all 0.1s ease',
- },
- iconOpen: {
- transition: 'all 0.1s ease',
- transform: 'rotate(90deg)',
- }
-});
-
-export interface SidePanelItem {
- id: string;
- name: string;
- url: string;
- icon: IconType;
- open?: boolean;
- margin?: boolean;
- openAble?: boolean;
- activeAction?: (dispatch: Dispatch, uuid?: string) => void;
-}
-
-interface SidePanelDataProps {
- toggleOpen: (id: string) => void;
- toggleActive: (id: string) => void;
- sidePanelItems: SidePanelItem[];
- onContextMenu: (event: React.MouseEvent<HTMLElement>, item: SidePanelItem) => void;
-}
-
-type SidePanelProps = RouteComponentProps<{}> & SidePanelDataProps & WithStyles<CssRules>;
-
-export const SidePanel = withStyles(styles)(withRouter(
- class extends React.Component<SidePanelProps> {
- render() {
- const { classes, toggleOpen, toggleActive, sidePanelItems, children } = this.props;
- const { root, row, list, toggableIconContainer } = classes;
-
- const path = this.props.location.pathname.split('/');
- const activeUrl = path.length > 1 ? "/" + path[1] : "/";
- return (
- <div className={root}>
- <List>
- {sidePanelItems.map(it => {
- const active = it.url === activeUrl;
- return <span key={it.name}>
- <ListItem button className={list} onClick={() => toggleActive(it.id)}
- onContextMenu={this.handleRowContextMenu(it)}>
- <span className={row}>
- {it.openAble ? (
- <i onClick={() => toggleOpen(it.id)} className={toggableIconContainer}>
- <ListItemIcon
- className={this.getToggableIconClassNames(it.open, active)}>
- < SidePanelRightArrowIcon/>
- </ListItemIcon>
- </i>
- ) : null}
- <ListItemTextIcon icon={it.icon} name={it.name} isActive={active}
- hasMargin={it.margin}/>
- </span>
- </ListItem>
- {it.openAble ? (
- <Collapse in={it.open} timeout="auto" unmountOnExit>
- {children}
- </Collapse>
- ) : null}
- </span>;
- })}
- </List>
- </div>
- );
- }
-
- getToggableIconClassNames = (isOpen?: boolean, isActive ?: boolean) => {
- const { iconOpen, iconClose, active, toggableIcon } = this.props.classes;
- return classnames(toggableIcon, {
- [iconOpen]: isOpen,
- [iconClose]: !isOpen,
- [active]: isActive
- });
- }
-
- handleRowContextMenu = (item: SidePanelItem) =>
- (event: React.MouseEvent<HTMLElement>) =>
- item.openAble ? this.props.onContextMenu(event, item) : null
- }
-));
--- /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/styles';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { Grid, Typography, Switch } from '@material-ui/core';
+
+type CssRules = 'grid' | 'label' | 'value' | 'switch';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ grid: {
+ display: 'flex'
+ },
+ label: {
+ width: '86px',
+ color: theme.palette.grey["500"],
+ textAlign: 'right'
+ },
+ value: {
+ width: '24px',
+ paddingLeft: theme.spacing.unit
+ },
+ switch: {
+ '& span:first-child': {
+ height: '18px'
+ }
+ }
+});
+
+export interface SubprocessFilterDataProps {
+ label: string;
+ value: number;
+ checked?: boolean;
+ key?: string;
+ onToggle?: () => void;
+}
+
+type SubprocessFilterProps = SubprocessFilterDataProps & WithStyles<CssRules>;
+
+export const SubprocessFilter = withStyles(styles)(
+ ({ classes, label, value, key, checked, onToggle }: SubprocessFilterProps) =>
+ <Grid item className={classes.grid} md={12} lg={6} >
+ <Typography component="span" className={classes.label}>{label}:</Typography>
+ <Typography component="span" className={classes.value}>{value}</Typography>
+ {onToggle && <Switch classes={{ root: classes.switch }}
+ checked={checked}
+ onChange={onToggle}
+ value={key}
+ color="primary" />
+ }
+ </Grid>
+);
\ No newline at end of file
import * as Adapter from 'enzyme-adapter-react-16';
import ListItem from "@material-ui/core/ListItem/ListItem";
-import { Tree, TreeItem } from './tree';
+import { Tree, TreeItem, TreeItemStatus } from './tree';
import { ProjectResource } from '../../models/project';
import { mockProjectResource } from '../../models/test-utils';
import { Checkbox } from '@material-ui/core';
id: "3",
open: true,
active: true,
- status: 1,
+ status: TreeItemStatus.LOADED
};
const wrapper = mount(<Tree
render={project => <div />}
id: "3",
open: true,
active: true,
- status: 1,
+ status: TreeItemStatus.LOADED,
};
const wrapper = mount(<Tree
render={project => <div />}
id: "3",
open: true,
active: true,
- status: 1,
+ status: TreeItemStatus.LOADED
};
const wrapper = mount(<Tree
showSelection={true}
id: "3",
open: true,
active: true,
- status: 1,
+ status: TreeItemStatus.LOADED,
};
const spy = jest.fn();
const onSelectionChanged = (event: any, item: TreeItem<any>) => spy(item);
id: "3",
open: true,
active: true,
- status: 1,
+ status: TreeItemStatus.LOADED,
});
});
});
export enum TreeItemStatus {
- INITIAL,
- PENDING,
- LOADED
+ INITIAL = 'initial',
+ PENDING = 'pending',
+ LOADED = 'loaded'
}
export interface TreeItem<T> {
onContextMenu={this.handleRowContextMenu(it)}>
{it.status === TreeItemStatus.PENDING ?
<CircularProgress size={10} className={loader} /> : null}
- <i onClick={() => this.props.toggleItemOpen(it.id, it.status)}
+ <i onClick={this.handleToggleItemOpen(it.id, it.status)}
className={toggableIconContainer}>
<ListItemIcon className={this.getToggableIconClassNames(it.open, it.active)}>
- {it.status !== TreeItemStatus.INITIAL && it.items && it.items.length === 0 ? <span /> : <SidePanelRightArrowIcon />}
+ {this.getProperArrowAnimation(it.status, it.items!)}
</ListItemIcon>
</i>
{this.props.showSelection &&
</List>;
}
+ getProperArrowAnimation = (status: string, items: Array<TreeItem<T>>) => {
+ return this.isSidePanelIconNotNeeded(status, items) ? <span /> : <SidePanelRightArrowIcon />;
+ }
+
+ isSidePanelIconNotNeeded = (status: string, items: Array<TreeItem<T>>) => {
+ return status === TreeItemStatus.PENDING ||
+ (status === TreeItemStatus.LOADED && !items) ||
+ (status === TreeItemStatus.LOADED && items && items.length === 0);
+ }
+
getToggableIconClassNames = (isOpen?: boolean, isActive?: boolean) => {
const { iconOpen, iconClose, active, toggableIcon } = this.props.classes;
return classnames(toggableIcon, {
}
: undefined;
}
+
+ handleToggleItemOpen = (id: string, status: TreeItemStatus) => (event: React.MouseEvent<HTMLElement>) => {
+ event.stopPropagation();
+ this.props.toggleItemOpen(id, status);
+ }
}
);
import { Provider } from "react-redux";
import { Workbench } from './views/workbench/workbench';
import './index.css';
-import { Route } from "react-router";
+import { Route } from 'react-router';
import createBrowserHistory from "history/createBrowserHistory";
-import { configureStore } from "./store/store";
+import { History } from "history";
+import { configureStore, RootStore } from './store/store';
import { ConnectedRouter } from "react-router-redux";
import { ApiToken } from "./views-components/api-token/api-token";
import { initAuth } from "./store/auth/auth-action";
import { createServices } from "./services/services";
-import { getProjectList } from "./store/project/project-action";
import { MuiThemeProvider } from '@material-ui/core/styles';
import { CustomTheme } from './common/custom-theme';
import { fetchConfig } from './common/config';
import { collectionFilesItemActionSet } from './views-components/context-menu/action-sets/collection-files-item-action-set';
import { collectionActionSet } from './views-components/context-menu/action-sets/collection-action-set';
import { collectionResourceActionSet } from './views-components/context-menu/action-sets/collection-resource-action-set';
+import { processActionSet } from './views-components/context-menu/action-sets/process-action-set';
+import { addRouteChangeHandlers } from './routes/routes';
+import { loadWorkbench } from './store/workbench/workbench-actions';
+import { Routes } from '~/routes/routes';
-const getBuildNumber = () => "BN-" + (process.env.BUILD_NUMBER || "dev");
-const getGitCommit = () => "GIT-" + (process.env.GIT_COMMIT || "latest").substr(0, 7);
+const getBuildNumber = () => "BN-" + (process.env.REACT_APP_BUILD_NUMBER || "dev");
+const getGitCommit = () => "GIT-" + (process.env.REACT_APP_GIT_COMMIT || "latest").substr(0, 7);
const getBuildInfo = () => getBuildNumber() + " / " + getGitCommit();
const buildInfo = getBuildInfo();
addMenuActionSet(ContextMenuKind.COLLECTION_FILES_ITEM, collectionFilesItemActionSet);
addMenuActionSet(ContextMenuKind.COLLECTION, collectionActionSet);
addMenuActionSet(ContextMenuKind.COLLECTION_RESOURCE, collectionResourceActionSet);
+addMenuActionSet(ContextMenuKind.PROCESS, processActionSet);
fetchConfig()
- .then(config => {
+ .then((config) => {
const history = createBrowserHistory();
const services = createServices(config);
const store = configureStore(history, services);
+ store.subscribe(initListener(history, store));
+
store.dispatch(initAuth());
- store.dispatch(getProjectList(services.authService.getUuid()));
- const TokenComponent = (props: any) => <ApiToken authService={services.authService} {...props}/>;
- const WorkbenchComponent = (props: any) => <Workbench authService={services.authService} buildInfo={buildInfo} {...props}/>;
+ const TokenComponent = (props: any) => <ApiToken authService={services.authService} {...props} />;
+ const WorkbenchComponent = (props: any) => <Workbench authService={services.authService} buildInfo={buildInfo} {...props} />;
const App = () =>
<MuiThemeProvider theme={CustomTheme}>
<Provider store={store}>
<ConnectedRouter history={history}>
<div>
- <Route path="/" component={WorkbenchComponent} />
- <Route path="/token" component={TokenComponent} />
+ <Route path={Routes.TOKEN} component={TokenComponent} />
+ <Route path={Routes.ROOT} component={WorkbenchComponent} />
</div>
</ConnectedRouter>
</Provider>
<App />,
document.getElementById('root') as HTMLElement
);
+
+
});
+const initListener = (history: History, store: RootStore) => {
+ let initialized = false;
+ return async () => {
+ const { router, auth } = store.getState();
+ if (router.location && auth.user && !initialized) {
+ initialized = true;
+ await store.dispatch(loadWorkbench());
+ addRouteChangeHandlers(history, store);
+ }
+ };
+};
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Resource, ResourceKind } from "./resource";
+import { MountType } from '~/models/mount-types';
+import { RuntimeConstraints } from "~/models/runtime-constraints";
+import { SchedulingParameters } from './scheduling-parameters';
+
+export enum ContainerState {
+ QUEUED = 'Queued',
+ LOCKED = 'Locked',
+ RUNNING = 'Running',
+ COMPLETE = 'Complete',
+ CANCELLED = 'Cancelled',
+}
+
+export interface ContainerResource extends Resource {
+ kind: ResourceKind.CONTAINER;
+ state: string;
+ startedAt: string | null;
+ finishedAt: string | null;
+ log: string | null;
+ environment: {};
+ cwd: string;
+ command: string[];
+ outputPath: string;
+ mounts: MountType[];
+ runtimeConstraints: RuntimeConstraints;
+ schedulingParameters: SchedulingParameters;
+ output: string | null;
+ containerImage: string;
+ progress: number;
+ priority: number;
+ exitCode: number | null;
+ authUuid: string | null;
+ lockedByUuid: string | null;
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export enum MountKind {
+ COLLECTION = 'collection',
+ GIT_TREE = 'git_tree',
+ TEMPORARY_DIRECTORY = 'tmp',
+ KEEP = 'keep',
+ MOUNTED_FILE = 'file',
+ JSON = 'JSON'
+}
+
+export type MountType =
+ CollectionMount |
+ GitTreeMount |
+ TemporaryDirectoryMount |
+ KeepMount |
+ JSONMount;
+
+export interface CollectionMount {
+ kind: MountKind.COLLECTION;
+ uuid?: string;
+ portableDataHash?: string;
+ path?: string;
+ writable?: boolean;
+}
+
+export interface GitTreeMount {
+ kind: MountKind.GIT_TREE;
+ uuid: string;
+ commit: string;
+ path?: string;
+}
+
+export enum TemporaryDirectoryDeviceType {
+ RAM = 'ram',
+ SSD = 'ssd',
+ DISK = 'disk',
+ NETWORK = 'network',
+}
+
+export interface TemporaryDirectoryMount {
+ kind: MountKind.TEMPORARY_DIRECTORY;
+ capacity: number;
+ deviceType: TemporaryDirectoryDeviceType;
+}
+
+export interface KeepMount {
+ kind: MountKind.KEEP;
+}
+
+export interface JSONMount {
+ kind: MountKind.JSON;
+ content: string;
+}
export enum ResourceKind {
COLLECTION = "arvados#collection",
+ CONTAINER = "arvados#container",
+ CONTAINER_REQUEST = "arvados#containerRequest",
GROUP = "arvados#group",
PROCESS = "arvados#containerRequest",
PROJECT = "arvados#group",
- WORKFLOW = "arvados#workflow"
+ USER = "arvados#user",
+ WORKFLOW = "arvados#workflow",
}
+
+export enum ResourceObjectType {
+ COLLECTION = '4zz18',
+ CONTAINER = 'dz642',
+ CONTAINER_REQUEST = 'xvhdp',
+ GROUP = 'j7d0g',
+ USER = 'tpzed',
+}
+
+export const RESOURCE_UUID_PATTERN = '.{5}-.{5}-.{15}';
+export const RESOURCE_UUID_REGEX = new RegExp(RESOURCE_UUID_PATTERN);
+
+export const isResourceUuid = (uuid: string) =>
+ RESOURCE_UUID_REGEX.test(uuid);
+
+export const extractUuidObjectType = (uuid: string) => {
+ const match = RESOURCE_UUID_REGEX.exec(uuid);
+ return match
+ ? match[0].split('-')[1]
+ : undefined;
+};
+
+export const extractUuidKind = (uuid: string = '') => {
+ const objectType = extractUuidObjectType(uuid);
+ switch (objectType) {
+ case ResourceObjectType.USER:
+ return ResourceKind.USER;
+ case ResourceObjectType.GROUP:
+ return ResourceKind.GROUP;
+ case ResourceObjectType.COLLECTION:
+ return ResourceKind.COLLECTION;
+ case ResourceObjectType.CONTAINER_REQUEST:
+ return ResourceKind.CONTAINER_REQUEST;
+ case ResourceObjectType.CONTAINER:
+ return ResourceKind.CONTAINER;
+ default:
+ return undefined;
+ }
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export interface RuntimeConstraints {
+ ram: number;
+ vcpus: number;
+ keepCacheRam: number;
+ API: boolean;
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export interface SchedulingParameters {
+ partitions: string[];
+ preemptible: boolean;
+ maxRunTime: number;
+}
{ children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' },
{ children: [], id: 'Node 3', parent: 'Node 2', value: 'Value 1' }
].reduce((tree, node) => Tree.setNode(node)(tree), tree);
- expect(Tree.getNodeAncestors('Node 3')(newTree)).toEqual(['Node 1', 'Node 2']);
+ expect(Tree.getNodeAncestorsIds('Node 3')(newTree)).toEqual(['Node 1', 'Node 2']);
});
it('gets node descendants', () => {
{ children: [], id: 'Node 3', parent: 'Node 1', value: 'Value 1' },
{ children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }
].reduce((tree, node) => Tree.setNode(node)(tree), tree);
- expect(Tree.getNodeDescendants('Node 1')(newTree)).toEqual(['Node 2', 'Node 3', 'Node 2.1', 'Node 3.1']);
+ expect(Tree.getNodeDescendantsIds('Node 1')(newTree)).toEqual(['Node 2', 'Node 3', 'Node 2.1', 'Node 3.1']);
});
it('gets root descendants', () => {
{ children: [], id: 'Node 3', parent: 'Node 1', value: 'Value 1' },
{ children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }
].reduce((tree, node) => Tree.setNode(node)(tree), tree);
- expect(Tree.getNodeDescendants('')(newTree)).toEqual(['Node 1', 'Node 2', 'Node 3', 'Node 2.1', 'Node 3.1']);
+ expect(Tree.getNodeDescendantsIds('')(newTree)).toEqual(['Node 1', 'Node 2', 'Node 3', 'Node 2.1', 'Node 3.1']);
});
it('gets node children', () => {
{ children: [], id: 'Node 3', parent: 'Node 1', value: 'Value 1' },
{ children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }
].reduce((tree, node) => Tree.setNode(node)(tree), tree);
- expect(Tree.getNodeChildren('Node 1')(newTree)).toEqual(['Node 2', 'Node 3']);
+ expect(Tree.getNodeChildrenIds('Node 1')(newTree)).toEqual(['Node 2', 'Node 3']);
});
it('gets root children', () => {
{ children: [], id: 'Node 3', parent: '', value: 'Value 1' },
{ children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }
].reduce((tree, node) => Tree.setNode(node)(tree), tree);
- expect(Tree.getNodeChildren('')(newTree)).toEqual(['Node 1', 'Node 3']);
+ expect(Tree.getNodeChildrenIds('')(newTree)).toEqual(['Node 1', 'Node 3']);
});
it('maps tree', () => {
export const TREE_ROOT_ID = '';
-export interface TreeNode<T> {
+export interface TreeNode<T = any> {
children: string[];
value: T;
id: string;
const [newTree] = [tree]
.map(tree => getNode(node.id)(tree) === node
? tree
- : {...tree, [node.id]: node})
+ : { ...tree, [node.id]: node })
.map(addChild(node.parent, node.id));
return newTree;
};
};
export const mapTreeValues = <T, R>(mapFn: (value: T) => R) => (tree: Tree<T>): Tree<R> =>
- getNodeDescendants('')(tree)
+ getNodeDescendantsIds('')(tree)
.map(id => getNode(id)(tree))
.map(mapNodeValue(mapFn))
.reduce((newTree, node) => setNode(node)(newTree), createTree<R>());
-export const mapTree = <T, R>(mapFn: (node: TreeNode<T>) => TreeNode<R>) => (tree: Tree<T>): Tree<R> =>
- getNodeDescendants('')(tree)
+export const mapTree = <T, R = T>(mapFn: (node: TreeNode<T>) => TreeNode<R>) => (tree: Tree<T>): Tree<R> =>
+ getNodeDescendantsIds('')(tree)
.map(id => getNode(id)(tree))
.map(mapFn)
.reduce((newTree, node) => setNode(node)(newTree), createTree<R>());
-export const getNodeAncestors = (id: string) => <T>(tree: Tree<T>): string[] => {
+export const getNodeAncestors = (id: string) => <T>(tree: Tree<T>) =>
+ mapIdsToNodes(getNodeAncestorsIds(id)(tree))(tree);
+
+
+export const getNodeAncestorsIds = (id: string) => <T>(tree: Tree<T>): string[] => {
const node = getNode(id)(tree);
return node && node.parent
- ? [...getNodeAncestors(node.parent)(tree), node.parent]
+ ? [...getNodeAncestorsIds(node.parent)(tree), node.parent]
: [];
};
-export const getNodeDescendants = (id: string, limit = Infinity) => <T>(tree: Tree<T>): string[] => {
+export const getNodeDescendants = (id: string, limit = Infinity) => <T>(tree: Tree<T>) =>
+ mapIdsToNodes(getNodeDescendantsIds(id, limit)(tree))(tree);
+
+export const getNodeDescendantsIds = (id: string, limit = Infinity) => <T>(tree: Tree<T>): string[] => {
const node = getNode(id)(tree);
const children = node ? node.children :
id === TREE_ROOT_ID
.concat(limit < 1
? []
: children
- .map(id => getNodeDescendants(id, limit - 1)(tree))
+ .map(id => getNodeDescendantsIds(id, limit - 1)(tree))
.reduce((nodes, nodeChildren) => [...nodes, ...nodeChildren], []));
};
-export const getNodeChildren = (id: string) => <T>(tree: Tree<T>): string[] =>
- getNodeDescendants(id, 0)(tree);
+export const getNodeChildren = (id: string) => <T>(tree: Tree<T>) =>
+ mapIdsToNodes(getNodeChildrenIds(id)(tree))(tree);
+
+export const getNodeChildrenIds = (id: string) => <T>(tree: Tree<T>): string[] =>
+ getNodeDescendantsIds(id, 0)(tree);
+
+export const mapIdsToNodes = (ids: string[]) => <T>(tree: Tree<T>) =>
+ ids.map(id => getNode(id)(tree)).filter((node): node is TreeNode<T> => node !== undefined);
const mapNodeValue = <T, R>(mapFn: (value: T) => R) => (node: TreeNode<T>): TreeNode<R> =>
({ ...node, value: mapFn(node.value) });
//
// SPDX-License-Identifier: AGPL-3.0
+import { Resource, ResourceKind } from '~/models/resource';
+
export interface User {
email: string;
firstName: string;
export const getUserFullname = (user?: User) => {
return user ? `${user.firstName} ${user.lastName}` : "";
-};
\ No newline at end of file
+};
+
+export interface UserResource extends Resource {
+ kind: ResourceKind.USER;
+ email: string;
+ username: string;
+ firstName: string;
+ lastName: string;
+ identityUrl: string;
+ isAdmin: boolean;
+ prefs: string;
+ defaultOwnerUuid: string;
+ isActive: boolean;
+ writableBy: string[];
+}
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { History, Location } from 'history';
+import { RootStore } from '~/store/store';
+import { matchPath } from 'react-router';
+import { ResourceKind, RESOURCE_UUID_PATTERN, extractUuidKind } from '~/models/resource';
+import { getProjectUrl } from '../models/project';
+import { getCollectionUrl } from '~/models/collection';
+import { loadProject, loadFavorites, loadCollection } from '~/store/workbench/workbench-actions';
+import { loadProcess } from '~/store/processes/processes-actions';
+
+export const Routes = {
+ ROOT: '/',
+ TOKEN: '/token',
+ PROJECTS: `/projects/:id(${RESOURCE_UUID_PATTERN})`,
+ COLLECTIONS: `/collections/:id(${RESOURCE_UUID_PATTERN})`,
+ PROCESSES: `/processes/:id(${RESOURCE_UUID_PATTERN})`,
+ FAVORITES: '/favorites',
+};
+
+export const getResourceUrl = (uuid: string) => {
+ const kind = extractUuidKind(uuid);
+ switch (kind) {
+ case ResourceKind.PROJECT:
+ return getProjectUrl(uuid);
+ case ResourceKind.COLLECTION:
+ return getCollectionUrl(uuid);
+ default:
+ return undefined;
+ }
+};
+
+export const getProcessUrl = (uuid: string) => `/processes/${uuid}`;
+
+export const addRouteChangeHandlers = (history: History, store: RootStore) => {
+ const handler = handleLocationChange(store);
+ handler(history.location);
+ history.listen(handler);
+};
+
+export const matchRootRoute = (route: string) =>
+ matchPath(route, { path: Routes.ROOT, exact: true });
+
+export const matchFavoritesRoute = (route: string) =>
+ matchPath(route, { path: Routes.FAVORITES });
+
+export interface ResourceRouteParams {
+ id: string;
+}
+
+export const matchProjectRoute = (route: string) =>
+ matchPath<ResourceRouteParams>(route, { path: Routes.PROJECTS });
+
+export const matchCollectionRoute = (route: string) =>
+ matchPath<ResourceRouteParams>(route, { path: Routes.COLLECTIONS });
+
+export const matchProcessRoute = (route: string) =>
+ matchPath<ResourceRouteParams>(route, { path: Routes.PROCESSES });
+
+
+const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
+ const projectMatch = matchProjectRoute(pathname);
+ const collectionMatch = matchCollectionRoute(pathname);
+ const favoriteMatch = matchFavoritesRoute(pathname);
+ const processMatch = matchProcessRoute(pathname);
+ if (projectMatch) {
+ store.dispatch(loadProject(projectMatch.params.id));
+ } else if (collectionMatch) {
+ store.dispatch(loadCollection(collectionMatch.params.id));
+ } else if (favoriteMatch) {
+ store.dispatch(loadFavorites());
+ } else if (processMatch) {
+ store.dispatch(loadProcess(processMatch.params.id));
+ }
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { GroupsService } from "~/services/groups-service/groups-service";
+import { UserService } from '../user-service/user-service';
+import { GroupResource } from '~/models/group';
+import { UserResource } from '~/models/user';
+import { extractUuidObjectType, ResourceObjectType } from "~/models/resource";
+
+export class AncestorService {
+ constructor(
+ private groupsService: GroupsService,
+ private userService: UserService
+ ) { }
+
+ 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
+ ];
+ }
+ } else {
+ return [];
+ }
+ }
+
+ private getService = (objectType?: string) => {
+ switch (objectType) {
+ case ResourceObjectType.GROUP:
+ return this.groupsService;
+ case ResourceObjectType.USER:
+ return this.userService;
+ default:
+ return undefined;
+ }
+ }
+}
\ No newline at end of file
import { uniqBy, groupBy } from 'lodash';
import { KeepManifestStream, KeepManifestStreamFile, KeepManifest } from "~/models/keep-manifest";
-import { TreeNode, setNode, createTree, getNodeDescendants, getNodeValue } from '~/models/tree';
+import { TreeNode, setNode, createTree, getNodeDescendantsIds, getNodeValue } from '~/models/tree';
import { CollectionFilesTree, CollectionFile, CollectionDirectory, createCollectionDirectory, createCollectionFile, CollectionFileType } from '../../models/collection-file';
export const mapCollectionFilesTreeToManifest = (tree: CollectionFilesTree): KeepManifest => {
- const values = getNodeDescendants('')(tree).map(id => getNodeValue(id)(tree));
+ const values = getNodeDescendantsIds('')(tree).map(id => getNodeValue(id)(tree));
const files = values.filter(value => value && value.type === CollectionFileType.FILE) as CollectionFile[];
const fileGroups = groupBy(files, file => file.path);
return Object
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { createCollectionFilesTree, CollectionDirectory, CollectionFile, CollectionFileType, createCollectionDirectory, createCollectionFile } from "../../models/collection-file";
+import { getTagValue } from "~/common/xml";
+import { getNodeChildren, Tree, mapTree } from '~/models/tree';
+
+export const parseFilesResponse = (document: Document) => {
+ const files = extractFilesData(document);
+ const tree = createCollectionFilesTree(files);
+ return sortFilesTree(tree);
+};
+
+export const sortFilesTree = (tree: Tree<CollectionDirectory | CollectionFile>) => {
+ return mapTree<CollectionDirectory | CollectionFile>(node => {
+ const children = getNodeChildren(node.id)(tree);
+
+ children.sort((a, b) =>
+ a.value.type !== b.value.type
+ ? a.value.type === CollectionFileType.DIRECTORY ? -1 : 1
+ : a.value.name.localeCompare(b.value.name)
+ );
+ return { ...node, children: children.map(child => child.id) };
+ })(tree);
+};
+
+export const extractFilesData = (document: Document) => {
+ const collectionUrlPrefix = /\/c=[0-9a-zA-Z\-]*/;
+ return Array
+ .from(document.getElementsByTagName('D:response'))
+ .slice(1) // omit first element which is collection itself
+ .map(element => {
+ const name = getTagValue(element, 'D:displayname', '');
+ const size = parseInt(getTagValue(element, 'D:getcontentlength', '0'), 10);
+ const url = getTagValue(element, 'D:href', '');
+ const nameSuffix = `/${name || ''}`;
+ const directory = url
+ .replace(collectionUrlPrefix, '')
+ .replace(nameSuffix, '');
+
+ const data = {
+ url,
+ id: `${directory}/${name}`,
+ name,
+ path: directory,
+ };
+
+ return getTagValue(element, 'D:resourcetype', '')
+ ? createCollectionDirectory(data)
+ : createCollectionFile({ ...data, size });
+
+ });
+};
//
// SPDX-License-Identifier: AGPL-3.0
-import * as _ from "lodash";
import { CommonResourceService } from "~/common/api/common-resource-service";
import { CollectionResource } from "~/models/collection";
-import axios, { AxiosInstance } from "axios";
-import { KeepService } from "../keep-service/keep-service";
+import { AxiosInstance } from "axios";
+import { CollectionFile, CollectionDirectory } from "~/models/collection-file";
import { WebDAV } from "~/common/webdav";
import { AuthService } from "../auth-service/auth-service";
-import { mapTree, getNodeChildren, getNode, TreeNode } from "../../models/tree";
-import { getTagValue } from "~/common/xml";
-import { FilterBuilder } from "~/common/api/filter-builder";
-import { CollectionFile, createCollectionFile, CollectionFileType, CollectionDirectory, createCollectionDirectory } from '~/models/collection-file';
-import { parseKeepManifestText, stringifyKeepManifest } from "../collection-files-service/collection-manifest-parser";
-import { KeepManifestStream } from "~/models/keep-manifest";
-import { createCollectionFilesTree } from '~/models/collection-file';
+import { mapTreeValues } from "~/models/tree";
+import { parseFilesResponse } from "./collection-service-files-response";
+import { fileToArrayBuffer } from "~/common/file";
export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
export class CollectionService extends CommonResourceService<CollectionResource> {
- constructor(serverApi: AxiosInstance, private keepService: KeepService, private webdavClient: WebDAV, private authService: AuthService) {
+ constructor(serverApi: AxiosInstance, private webdavClient: WebDAV, private authService: AuthService) {
super(serverApi, "collections");
}
async files(uuid: string) {
- const request = await this.webdavClient.propfind(`/c=${uuid}`);
+ const request = await this.webdavClient.propfind(`c=${uuid}`);
if (request.responseXML != null) {
- const files = this.extractFilesData(request.responseXML);
- const tree = createCollectionFilesTree(files);
- const sortedTree = mapTree(node => {
- const children = getNodeChildren(node.id)(tree).map(id => getNode(id)(tree)) as TreeNode<CollectionDirectory | CollectionFile>[];
- children.sort((a, b) =>
- a.value.type !== b.value.type
- ? a.value.type === CollectionFileType.DIRECTORY ? -1 : 1
- : a.value.name.localeCompare(b.value.name)
- );
- return { ...node, children: children.map(child => child.id) };
- })(tree);
- return sortedTree;
+ const filesTree = parseFilesResponse(request.responseXML);
+ return mapTreeValues(this.extendFileURL)(filesTree);
}
return Promise.reject();
}
- async deleteFile(collectionUuid: string, filePath: string) {
- return this.webdavClient.delete(`/c=${collectionUuid}${filePath}`);
- }
-
- extractFilesData(document: Document) {
- const collectionUrlPrefix = /\/c=[0-9a-zA-Z\-]*/;
- return Array
- .from(document.getElementsByTagName('D:response'))
- .slice(1) // omit first element which is collection itself
- .map(element => {
- const name = getTagValue(element, 'D:displayname', '');
- const size = parseInt(getTagValue(element, 'D:getcontentlength', '0'), 10);
- const pathname = getTagValue(element, 'D:href', '');
- const nameSuffix = `/${name || ''}`;
- const directory = pathname
- .replace(collectionUrlPrefix, '')
- .replace(nameSuffix, '');
- const href = this.webdavClient.defaults.baseURL + pathname + '?api_token=' + this.authService.getApiToken();
-
- const data = {
- url: href,
- id: `${directory}/${name}`,
- name,
- path: directory,
- };
-
- return getTagValue(element, 'D:resourcetype', '')
- ? createCollectionDirectory(data)
- : createCollectionFile({ ...data, size });
-
- });
+ async deleteFiles(collectionUuid: string, filePaths: string[]) {
+ for (const path of filePaths) {
+ await this.webdavClient.delete(`c=${collectionUuid}${path}`);
+ }
}
- private readFile(file: File): Promise<ArrayBuffer> {
- return new Promise<ArrayBuffer>(resolve => {
- const reader = new FileReader();
- reader.onload = () => {
- resolve(reader.result as ArrayBuffer);
- };
-
- reader.readAsArrayBuffer(file);
- });
+ async uploadFiles(collectionUuid: string, files: File[], onProgress?: UploadProgress) {
+ // files have to be uploaded sequentially
+ for (let idx = 0; idx < files.length; idx++) {
+ await this.uploadFile(collectionUuid, files[idx], idx, onProgress);
+ }
}
- private uploadFile(keepServiceHost: string, file: File, fileId: number, onProgress?: UploadProgress): Promise<CollectionFile> {
- return this.readFile(file).then(content => {
- return axios.post<string>(keepServiceHost, content, {
- headers: {
- 'Content-Type': 'text/octet-stream'
- },
- onUploadProgress: (e: ProgressEvent) => {
- if (onProgress) {
- onProgress(fileId, e.loaded, e.total, Date.now());
- }
- console.log(`${e.loaded} / ${e.total}`);
- }
- }).then(data => createCollectionFile({
- id: data.data,
- name: file.name,
- size: file.size
- }));
- });
+ moveFile(collectionUuid: string, oldPath: string, newPath: string) {
+ return this.webdavClient.move(
+ `c=${collectionUuid}${oldPath}`,
+ `c=${collectionUuid}${encodeURI(newPath)}`
+ );
}
- private async updateManifest(collectionUuid: string, files: CollectionFile[]): Promise<CollectionResource> {
- const collection = await this.get(collectionUuid);
- const manifest: KeepManifestStream[] = parseKeepManifestText(collection.manifestText);
-
- files.forEach(f => {
- let kms = manifest.find(stream => stream.name === f.path);
- if (!kms) {
- kms = {
- files: [],
- locators: [],
- name: f.path
- };
- manifest.push(kms);
+ private extendFileURL = (file: CollectionDirectory | CollectionFile) => ({
+ ...file,
+ url: this.webdavClient.defaults.baseURL + file.url + '?api_token=' + this.authService.getApiToken()
+ })
+
+ private async uploadFile(collectionUuid: string, file: File, fileId: number, onProgress: UploadProgress = () => { return; }) {
+ const fileURL = `c=${collectionUuid}/${file.name}`;
+ const fileContent = await fileToArrayBuffer(file);
+ const requestConfig = {
+ headers: {
+ 'Content-Type': 'text/octet-stream'
+ },
+ onUploadProgress: (e: ProgressEvent) => {
+ onProgress(fileId, e.loaded, e.total, Date.now());
}
- kms.locators.push(f.id);
- const len = kms.files.length;
- const nextPos = len > 0
- ? parseInt(kms.files[len - 1].position, 10) + kms.files[len - 1].size
- : 0;
- kms.files.push({
- name: f.name,
- position: nextPos.toString(),
- size: f.size
- });
- });
-
- console.log(manifest);
-
- const manifestText = stringifyKeepManifest(manifest);
- const data = { ...collection, manifestText };
- return this.update(collectionUuid, CommonResourceService.mapKeys(_.snakeCase)(data));
- }
-
- uploadFiles(collectionUuid: string, files: File[], onProgress?: UploadProgress): Promise<CollectionResource | never> {
- const filters = new FilterBuilder()
- .addEqual("service_type", "proxy");
-
- return this.keepService.list({ filters: filters.getFilters() }).then(data => {
- if (data.items && data.items.length > 0) {
- const serviceHost =
- (data.items[0].serviceSslFlag ? "https://" : "http://") +
- data.items[0].serviceHost +
- ":" + data.items[0].servicePort;
-
- console.log("serviceHost", serviceHost);
+ };
+ return this.webdavClient.put(fileURL, fileContent, requestConfig);
- const files$ = files.map((f, idx) => this.uploadFile(serviceHost, f, idx, onProgress));
- return Promise.all(files$).then(values => {
- return this.updateManifest(collectionUuid, values);
- });
- } else {
- return Promise.reject("Missing keep service host");
- }
- });
}
trash(uuid: string): Promise<CollectionResource> {
})
.then(CommonResourceService.mapResponseKeys);
}
-
+
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { CommonResourceService } from "~/common/api/common-resource-service";
+import { AxiosInstance } from "axios";
+import { ContainerRequestResource } from '../../models/container-request';
+
+export class ContainerRequestService extends CommonResourceService<ContainerRequestResource> {
+ constructor(serverApi: AxiosInstance) {
+ super(serverApi, "container_requests");
+ }
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { CommonResourceService } from "~/common/api/common-resource-service";
+import { AxiosInstance } from "axios";
+import { ContainerResource } from '../../models/container';
+
+export class ContainerService extends CommonResourceService<ContainerResource> {
+ constructor(serverApi: AxiosInstance) {
+ super(serverApi, "containers");
+ }
+}
//
// SPDX-License-Identifier: AGPL-3.0
-import Axios, { AxiosInstance } from "axios";
+import Axios from "axios";
import { AuthService } from "./auth-service/auth-service";
import { GroupsService } from "./groups-service/groups-service";
import { ProjectService } from "./project-service/project-service";
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';
export type ServiceRepository = ReturnType<typeof createServices>;
export const createServices = (config: Config) => {
const apiClient = Axios.create();
- apiClient.defaults.baseURL = `${config.apiHost}/arvados/v1`;
+ apiClient.defaults.baseURL = config.baseUrl;
const webdavClient = new WebDAV();
- webdavClient.defaults.baseURL = config.keepWebHost;
+ webdavClient.defaults.baseURL = config.keepWebServiceUrl;
- const authService = new AuthService(apiClient, config.apiHost);
- const keepService = new KeepService(apiClient);
const groupsService = new GroupsService(apiClient);
- const projectService = new ProjectService(apiClient);
+ const keepService = new KeepService(apiClient);
const linkService = new LinkService(apiClient);
+ const projectService = new ProjectService(apiClient);
+ const userService = new UserService(apiClient);
+ const containerRequestService = new ContainerRequestService(apiClient);
+ const containerService = new ContainerService(apiClient);
+
+ const ancestorsService = new AncestorService(groupsService, userService);
+ const authService = new AuthService(apiClient, config.rootUrl);
+ const collectionService = new CollectionService(apiClient, webdavClient, authService);
+ const collectionFilesService = new CollectionFilesService(collectionService);
const favoriteService = new FavoriteService(linkService, groupsService);
- const collectionService = new CollectionService(apiClient, keepService, webdavClient, authService);
const tagService = new TagService(linkService);
- const collectionFilesService = new CollectionFilesService(collectionService);
return {
+ ancestorsService,
apiClient,
- webdavClient,
authService,
- keepService,
+ collectionFilesService,
+ collectionService,
+ containerRequestService,
+ containerService,
+ favoriteService,
groupsService,
- projectService,
+ keepService,
linkService,
- favoriteService,
- collectionService,
+ projectService,
tagService,
- collectionFilesService
+ userService,
+ webdavClient,
};
};
+export const getResourceService = (kind?: ResourceKind) => (serviceRepository: ServiceRepository) => {
+ switch (kind) {
+ case ResourceKind.USER:
+ return serviceRepository.userService;
+ case ResourceKind.GROUP:
+ return serviceRepository.groupsService;
+ case ResourceKind.COLLECTION:
+ return serviceRepository.collectionService;
+ default:
+ return undefined;
+ }
+};
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { AxiosInstance } from "axios";
+import { CommonResourceService } from "~/common/api/common-resource-service";
+import { UserResource } from "~/models/user";
+
+export class UserService extends CommonResourceService<UserResource> {
+ constructor(serverApi: AxiosInstance) {
+ super(serverApi, "users");
+ }
+}
\ No newline at end of file
//
// SPDX-License-Identifier: AGPL-3.0
-import { ofType, default as unionize, UnionOf } from "unionize";
+import { ofType, unionize, UnionOf } from '~/common/unionize';
import { Dispatch } from "redux";
import { User } from "~/models/user";
import { RootState } from "../store";
INIT: ofType<{ user: User, token: string }>(),
USER_DETAILS_REQUEST: {},
USER_DETAILS_SUCCESS: ofType<User>()
-}, {
- tag: 'type',
- value: 'payload'
});
function setAuthorizationHeader(services: ServiceRepository, token: string) {
import { createServices } from "~/services/services";
import { configureStore, RootStore } from "../store";
import createBrowserHistory from "history/createBrowserHistory";
+import { mockConfig } from '~/common/config';
describe('auth-actions', () => {
let reducer: (state: AuthState | undefined, action: AuthAction) => any;
let store: RootStore;
beforeEach(() => {
- store = configureStore(createBrowserHistory(), createServices({ apiHost: "/arvados/v1", keepWebHost: "" }));
+ store = configureStore(createBrowserHistory(), createServices(mockConfig({})));
localStorage.clear();
- reducer = authReducer(createServices({ apiHost: "/arvados/v1", keepWebHost: "" }));
+ reducer = authReducer(createServices(mockConfig({})));
});
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';
describe('auth-reducer', () => {
let reducer: (state: AuthState | undefined, action: AuthAction) => any;
beforeAll(() => {
localStorage.clear();
- reducer = authReducer(createServices({ apiHost: "/arvados/v1", keepWebHost: "" }));
+ reducer = authReducer(createServices(mockConfig({})));
});
it('should correctly initialise state', () => {
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { 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 { propertiesActions } from '../properties/properties-actions';
+
+export const BREADCRUMBS = 'breadcrumbs';
+
+export interface ResourceBreadcrumb extends Breadcrumb {
+ uuid: string;
+}
+
+export const setBreadcrumbs = (breadcrumbs: Breadcrumb[]) =>
+ propertiesActions.SET_PROPERTY({ key: BREADCRUMBS, value: breadcrumbs });
+
+const getSidePanelTreeBreadcrumbs = (uuid: string) => (treePicker: TreePicker): ResourceBreadcrumb[] => {
+ const nodes = getSidePanelTreeBranch(uuid)(treePicker);
+ return nodes.map(node =>
+ typeof node.value === 'string'
+ ? { label: node.value, uuid: node.nodeId }
+ : { label: node.value.name, uuid: node.value.uuid });
+};
+
+export const setSidePanelBreadcrumbs = (uuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const { treePicker } = getState();
+ const breadcrumbs = getSidePanelTreeBreadcrumbs(uuid)(treePicker);
+ dispatch(setBreadcrumbs(breadcrumbs));
+ };
+
+export const setProjectBreadcrumbs = setSidePanelBreadcrumbs;
+
+export const setCollectionBreadcrumbs = (collectionUuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const { resources } = getState();
+ const collection = getResource(collectionUuid)(resources);
+ if (collection) {
+ dispatch<any>(setProjectBreadcrumbs(collection.ownerUuid));
+ }
+ };
//
// SPDX-License-Identifier: AGPL-3.0
-import { unionize, ofType, UnionOf } from "unionize";
import { Dispatch } from "redux";
import { loadCollectionFiles } from "./collection-panel-files/collection-panel-files-actions";
-import { CollectionResource } from "~/models/collection";
+import { CollectionResource } from '~/models/collection';
import { collectionPanelFilesAction } from "./collection-panel-files/collection-panel-files-actions";
import { createTree } from "~/models/tree";
import { RootState } from "../store";
import { ServiceRepository } from "~/services/services";
import { TagResource, TagProperty } from "~/models/tag";
import { snackbarActions } from "../snackbar/snackbar-actions";
+import { resourcesActions } from "~/store/resources/resources-actions";
+import { unionize, ofType, UnionOf } from '~/common/unionize';
export const collectionPanelActions = unionize({
LOAD_COLLECTION: ofType<{ uuid: string }>(),
CREATE_COLLECTION_TAG_SUCCESS: ofType<{ tag: TagResource }>(),
DELETE_COLLECTION_TAG: ofType<{ uuid: string }>(),
DELETE_COLLECTION_TAG_SUCCESS: ofType<{ uuid: string }>()
-}, { tag: 'type', value: 'payload' });
+});
export type CollectionPanelAction = UnionOf<typeof collectionPanelActions>;
export const COLLECTION_TAG_FORM_NAME = 'collectionTagForm';
-export const loadCollection = (uuid: string) =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+export const loadCollectionPanel = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid }));
dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() }));
- return services.collectionService
- .get(uuid)
- .then(item => {
- dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item }));
- dispatch<any>(loadCollectionFiles(uuid));
- });
+ const collection = await services.collectionService.get(uuid);
+ dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: collection }));
+ dispatch(resourcesActions.SET_RESOURCES([collection]));
+ dispatch<any>(loadCollectionFiles(collection.uuid));
+ dispatch<any>(loadCollectionTags(collection.uuid));
+ return collection;
};
export const loadCollectionTags = (uuid: string) =>
});
};
-
export const createCollectionTag = (data: TagProperty) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(collectionPanelActions.CREATE_COLLECTION_TAG({ data }));
//
// SPDX-License-Identifier: AGPL-3.0
-import { default as unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
import { Dispatch } from "redux";
import { CollectionFilesTree, CollectionFileType } from "~/models/collection-file";
import { ServiceRepository } from "~/services/services";
import { RootState } from "../../store";
import { snackbarActions } from "../../snackbar/snackbar-actions";
-import { dialogActions } from "../../dialog/dialog-actions";
-import { getNodeValue, getNodeDescendants } from "~/models/tree";
-import { CollectionPanelDirectory, CollectionPanelFile } from "./collection-panel-files-state";
+import { dialogActions } from '../../dialog/dialog-actions';
+import { getNodeValue } from "~/models/tree";
+import { filterCollectionFilesBySelection } from './collection-panel-files-state';
+import { startSubmit, initialize, stopSubmit, reset } from 'redux-form';
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/common/api/common-resource-service";
+import { getDialog } from "~/store/dialog/dialog-reducer";
+import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
export const collectionPanelFilesAction = unionize({
SET_COLLECTION_FILES: ofType<CollectionFilesTree>(),
TOGGLE_COLLECTION_FILE_SELECTION: ofType<{ id: string }>(),
SELECT_ALL_COLLECTION_FILES: ofType<{}>(),
UNSELECT_ALL_COLLECTION_FILES: ofType<{}>(),
-}, { tag: 'type', value: 'payload' });
+});
export type CollectionPanelFilesAction = UnionOf<typeof collectionPanelFilesAction>;
export const removeCollectionFiles = (filePaths: string[]) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const { item } = getState().collectionPanel;
- if (item) {
+ const currentCollection = getState().collectionPanel.item;
+ if (currentCollection) {
dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
- const promises = filePaths.map(filePath => services.collectionService.deleteFile(item.uuid, filePath));
- await Promise.all(promises);
- dispatch<any>(loadCollectionFiles(item.uuid));
+ await services.collectionService.deleteFiles(currentCollection.uuid, filePaths);
+ dispatch<any>(loadCollectionFiles(currentCollection.uuid));
dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000 }));
}
};
export const removeCollectionsSelectedFiles = () =>
(dispatch: Dispatch, getState: () => RootState) => {
- const tree = getState().collectionPanelFiles;
- const allFiles = getNodeDescendants('')(tree)
- .map(id => getNodeValue(id)(tree))
- .filter(file => file !== undefined) as Array<CollectionPanelDirectory | CollectionPanelFile>;
-
- const selectedDirectories = allFiles.filter(file => file.selected && file.type === CollectionFileType.DIRECTORY);
- const selectedFiles = allFiles.filter(file => file.selected && !selectedDirectories.some(dir => dir.id === file.path));
- const paths = [...selectedDirectories, ...selectedFiles].map(file => file.id);
+ const paths = filterCollectionFilesBySelection(getState().collectionPanelFiles, true).map(file => file.id);
dispatch<any>(removeCollectionFiles(paths));
};
export const FILE_REMOVE_DIALOG = 'fileRemoveDialog';
+
export const openFileRemoveDialog = (filePath: string) =>
(dispatch: Dispatch, getState: () => RootState) => {
const file = getNodeValue(filePath)(getState().collectionPanelFiles);
};
export const MULTIPLE_FILES_REMOVE_DIALOG = 'multipleFilesRemoveDialog';
+
export const openMultipleFilesRemoveDialog = () =>
dialogActions.OPEN_DIALOG({
id: MULTIPLE_FILES_REMOVE_DIALOG,
confirmButtonLabel: 'Remove'
}
});
+
+export const RENAME_FILE_DIALOG = 'renameFileDialog';
+export interface RenameFileDialogData {
+ name: string;
+ id: string;
+}
+
+export const openRenameFileDialog = (data: RenameFileDialogData) =>
+ (dispatch: Dispatch) => {
+ dispatch(reset(RENAME_FILE_DIALOG));
+ dispatch(dialogActions.OPEN_DIALOG({ id: RENAME_FILE_DIALOG, data }));
+ };
+
+export const renameFile = (newName: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const dialog = getDialog<RenameFileDialogData>(getState().dialog, RENAME_FILE_DIALOG);
+ const currentCollection = getState().collectionPanel.item;
+ if (dialog && currentCollection) {
+ dispatch(startSubmit(RENAME_FILE_DIALOG));
+ const oldPath = dialog.data.id;
+ const newPath = dialog.data.id.replace(dialog.data.name, newName);
+ try {
+ await services.collectionService.moveFile(currentCollection.uuid, oldPath, newPath);
+ dispatch<any>(loadCollectionFiles(currentCollection.uuid));
+ dispatch(dialogActions.CLOSE_DIALOG({ id: RENAME_FILE_DIALOG }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'File name changed.', hideDuration: 2000 }));
+ } catch (e) {
+ dispatch(stopSubmit(RENAME_FILE_DIALOG, { name: 'Could not rename the file' }));
+ }
+ }
+ };
//
// SPDX-License-Identifier: AGPL-3.0
-import { CollectionPanelFilesState, CollectionPanelFile, CollectionPanelDirectory, mapCollectionFileToCollectionPanelFile, mergeCollectionPanelFilesStates } from "./collection-panel-files-state";
+import { CollectionPanelFilesState, CollectionPanelFile, CollectionPanelDirectory, mapCollectionFileToCollectionPanelFile, mergeCollectionPanelFilesStates } from './collection-panel-files-state';
import { CollectionPanelFilesAction, collectionPanelFilesAction } from "./collection-panel-files-actions";
-import { createTree, mapTreeValues, getNode, setNode, getNodeAncestors, getNodeDescendants, setNodeValueWith, mapTree } from "~/models/tree";
+import { createTree, mapTreeValues, getNode, setNode, getNodeAncestorsIds, getNodeDescendantsIds, setNodeValueWith, mapTree } from "~/models/tree";
import { CollectionFileType } from "~/models/collection-file";
export const collectionPanelFilesReducer = (state: CollectionPanelFilesState = createTree(), action: CollectionPanelFilesAction) => {
const toggleDescendants = (id: string) => (tree: CollectionPanelFilesState) => {
const node = getNode(id)(tree);
if (node && node.value.type === CollectionFileType.DIRECTORY) {
- return getNodeDescendants(id)(tree)
+ return getNodeDescendantsIds(id)(tree)
.reduce((newTree, id) =>
setNodeValueWith(v => ({ ...v, selected: node.value.selected }))(id)(newTree), tree);
}
};
const toggleAncestors = (id: string) => (tree: CollectionPanelFilesState) => {
- const ancestors = getNodeAncestors(id)(tree).reverse();
+ const ancestors = getNodeAncestorsIds(id)(tree).reverse();
return ancestors.reduce((newTree, parent) => parent ? toggleParentNode(parent)(newTree) : newTree, tree);
};
//
// SPDX-License-Identifier: AGPL-3.0
-import { Tree, TreeNode, mapTreeValues, getNodeValue } from '~/models/tree';
+import { Tree, TreeNode, mapTreeValues, getNodeValue, getNodeDescendants } from '~/models/tree';
import { CollectionFile, CollectionDirectory, CollectionFileType } from '~/models/collection-file';
export type CollectionPanelFilesState = Tree<CollectionPanelDirectory | CollectionPanelFile>;
: { ...value, selected: oldValue.selected }
: value;
})(newState);
-};
+};
+
+export const filterCollectionFilesBySelection = (tree: CollectionPanelFilesState, selected: boolean) => {
+ const allFiles = getNodeDescendants('')(tree).map(node => node.value);
+
+ const selectedDirectories = allFiles.filter(file => file.selected === selected && file.type === CollectionFileType.DIRECTORY);
+ const selectedFiles = allFiles.filter(file => file.selected === selected && !selectedDirectories.some(dir => dir.id === file.path));
+ return [...selectedDirectories, ...selectedFiles];
+};
--- /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, stopSubmit } 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 { getCommonResourceServiceError, CommonResourceServiceError } from '~/common/api/common-resource-service';
+
+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 };
+ dispatch<any>(initialize(COLLECTION_COPY_FORM_NAME, initialData));
+ dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_COPY_FORM_NAME, data: {} }));
+ };
+
+export const copyCollection = (resource: CollectionCopyFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(startSubmit(COLLECTION_COPY_FORM_NAME));
+ try {
+ 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 }));
+ return collection;
+ } catch (e) {
+ const error = getCommonResourceServiceError(e);
+ if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+ dispatch(stopSubmit(COLLECTION_COPY_FORM_NAME, { ownerUuid: 'A collection with the same name already exists in the target project.' }));
+ } else {
+ dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_COPY_FORM_NAME }));
+ throw new Error('Could not copy the collection.');
+ }
+ 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 { reset, startSubmit, stopSubmit, initialize } from 'redux-form';
+import { RootState } from '~/store/store';
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { ServiceRepository } from '~/services/services';
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/common/api/common-resource-service";
+import { uploadCollectionFiles } from './collection-upload-actions';
+import { fileUploaderActions } from '~/store/file-uploader/file-uploader-actions';
+
+export interface CollectionCreateFormDialogData {
+ ownerUuid: string;
+ name: string;
+ description: string;
+}
+
+export const COLLECTION_CREATE_FORM_NAME = "collectionCreateFormName";
+
+export const openCollectionCreateDialog = (ownerUuid: string) =>
+ (dispatch: Dispatch) => {
+ dispatch(initialize(COLLECTION_CREATE_FORM_NAME, { ownerUuid }));
+ dispatch(fileUploaderActions.CLEAR_UPLOAD());
+ dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_CREATE_FORM_NAME, data: { ownerUuid } }));
+ };
+
+export const createCollection = (data: CollectionCreateFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(startSubmit(COLLECTION_CREATE_FORM_NAME));
+ try {
+ 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));
+ 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 ;
+ }
+ };
--- /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 "~/common/api/common-resource-service";
+import { snackbarActions } 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';
+
+export const COLLECTION_MOVE_FORM_NAME = 'collectionMoveFormName';
+
+export const openMoveCollectionDialog = (resource: { name: string, uuid: string }) =>
+ (dispatch: Dispatch) => {
+ dispatch<any>(resetPickerProjectTree());
+ dispatch(initialize(COLLECTION_MOVE_FORM_NAME, resource));
+ dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_MOVE_FORM_NAME, data: {} }));
+ };
+
+export const moveCollection = (resource: MoveToFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(startSubmit(COLLECTION_MOVE_FORM_NAME));
+ try {
+ 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 }));
+ return collection;
+ } catch (e) {
+ const error = getCommonResourceServiceError(e);
+ if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+ dispatch(stopSubmit(COLLECTION_MOVE_FORM_NAME, { ownerUuid: 'A collection with the same name already exists in the target project.' }));
+ } else {
+ dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_MOVE_FORM_NAME }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not move the collection.', hideDuration: 2000 }));
+ }
+ return ;
+ }
+ };
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { initialize, startSubmit, stopSubmit } from 'redux-form';
+import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
+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 { getCommonResourceServiceError, CommonResourceServiceError } from '~/common/api/common-resource-service';
+
+export const COLLECTION_PARTIAL_COPY_FORM_NAME = 'COLLECTION_PARTIAL_COPY_DIALOG';
+
+export interface CollectionPartialCopyFormData {
+ name: string;
+ description: string;
+ projectUuid: string;
+}
+
+export const openCollectionPartialCopyDialog = () =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const currentCollection = getState().collectionPanel.item;
+ if (currentCollection) {
+ const initialData = {
+ name: currentCollection.name,
+ description: currentCollection.description,
+ projectUuid: ''
+ };
+ dispatch(initialize(COLLECTION_PARTIAL_COPY_FORM_NAME, initialData));
+ dispatch<any>(resetPickerProjectTree());
+ dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME, data: {} }));
+ }
+ };
+
+export const copyCollectionPartial = ({ name, description, projectUuid }: CollectionPartialCopyFormData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(startSubmit(COLLECTION_PARTIAL_COPY_FORM_NAME));
+ const state = getState();
+ const currentCollection = state.collectionPanel.item;
+ if (currentCollection) {
+ try {
+ const collection = await services.collectionService.get(currentCollection.uuid);
+ const collectionCopy = {
+ ...collection,
+ name,
+ description,
+ ownerUuid: projectUuid,
+ uuid: undefined
+ };
+ const newCollection = await services.collectionService.create(collectionCopy);
+ 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 }));
+ } catch (e) {
+ const error = getCommonResourceServiceError(e);
+ if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+ dispatch(stopSubmit(COLLECTION_PARTIAL_COPY_FORM_NAME, { name: 'Collection with this name already exists.' }));
+ } else if (error === CommonResourceServiceError.UNKNOWN) {
+ dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not create a copy of collection', hideDuration: 2000 }));
+ } else {
+ 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 }));
+ }
+ }
+ }
+ };
\ 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 { collectionPanelActions } from "~/store/collection-panel/collection-panel-action";
+import { loadDetailsPanel } from "~/store/details-panel/details-panel-action";
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { dataExplorerActions } from "~/store/data-explorer/data-explorer-action";
+import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { ContextMenuResource } from '~/store/context-menu/context-menu-reducer';
+import { PROJECT_PANEL_ID } from "~/views/project-panel/project-panel";
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/common/api/common-resource-service";
+import { ServiceRepository } from "~/services/services";
+import { CollectionResource } from '~/models/collection';
+
+export interface CollectionUpdateFormDialogData {
+ uuid: string;
+ name: string;
+ description: string;
+}
+
+export const COLLECTION_UPDATE_FORM_NAME = 'collectionUpdateFormName';
+
+export const openCollectionUpdateDialog = (resource: ContextMenuResource) =>
+ (dispatch: Dispatch) => {
+ dispatch(initialize(COLLECTION_UPDATE_FORM_NAME, resource));
+ dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_UPDATE_FORM_NAME, data: {} }));
+ };
+
+export const updateCollection = (collection: Partial<CollectionResource>) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const uuid = collection.uuid || '';
+ dispatch(startSubmit(COLLECTION_UPDATE_FORM_NAME));
+ try {
+ 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 }));
+ 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.' }));
+ }
+ 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 { RootState } from '~/store/store';
+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 { fileUploaderActions } from '~/store/file-uploader/file-uploader-actions';
+import { reset, startSubmit } from 'redux-form';
+
+export const uploadCollectionFiles = (collectionUuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(fileUploaderActions.START_UPLOAD());
+ const files = getState().fileUploader.map(file => file.file);
+ await services.collectionService.uploadFiles(collectionUuid, files, handleUploadProgress(dispatch));
+ dispatch(fileUploaderActions.CLEAR_UPLOAD());
+ };
+
+export const COLLECTION_UPLOAD_FILES_DIALOG = 'uploadCollectionFilesDialog';
+
+export const openUploadCollectionFilesDialog = () => (dispatch: Dispatch) => {
+ dispatch(reset(COLLECTION_UPLOAD_FILES_DIALOG));
+ dispatch(fileUploaderActions.CLEAR_UPLOAD());
+ dispatch<any>(dialogActions.OPEN_DIALOG({ id: COLLECTION_UPLOAD_FILES_DIALOG, data: {} }));
+};
+
+export const submitCollectionFiles = () =>
+ 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 }));
+ }
+ };
+
+export const closeUploadCollectionFilesDialog = () => dialogActions.CLOSE_DIALOG({ id: 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
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { combineReducers } from 'redux';
-import { collectionCreatorReducer, CollectionCreatorState } from "./creator/collection-creator-reducer";
-import { collectionUpdaterReducer, CollectionUpdaterState } from "./updater/collection-updater-reducer";
-import { collectionUploaderReducer, CollectionUploaderState } from "./uploader/collection-uploader-reducer";
-
-export type CollectionsState = {
- creator: CollectionCreatorState;
- updater: CollectionUpdaterState;
- uploader: CollectionUploaderState
-};
-
-export const collectionsReducer = combineReducers({
- creator: collectionCreatorReducer,
- updater: collectionUpdaterReducer,
- uploader: collectionUploaderReducer
-});
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { default as unionize, ofType, UnionOf } from "unionize";
-import { Dispatch } from "redux";
-
-import { RootState } from "../../store";
-import { CollectionResource } from '~/models/collection';
-import { ServiceRepository } from "~/services/services";
-import { collectionUploaderActions } from "../uploader/collection-uploader-actions";
-import { reset } from "redux-form";
-
-export const collectionCreateActions = unionize({
- OPEN_COLLECTION_CREATOR: ofType<{ ownerUuid: string }>(),
- CLOSE_COLLECTION_CREATOR: ofType<{}>(),
- CREATE_COLLECTION: ofType<{}>(),
- CREATE_COLLECTION_SUCCESS: ofType<{}>(),
-}, {
- tag: 'type',
- value: 'payload'
-});
-
-export const createCollection = (collection: Partial<CollectionResource>, files: File[]) =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const { ownerUuid } = getState().collections.creator;
- const collectiontData = { ownerUuid, ...collection };
- dispatch(collectionCreateActions.CREATE_COLLECTION(collectiontData));
- return services.collectionService
- .create(collectiontData)
- .then(collection => {
- dispatch(collectionUploaderActions.START_UPLOAD());
- services.collectionService.uploadFiles(collection.uuid, files,
- (fileId, loaded, total, currentTime) => {
- dispatch(collectionUploaderActions.SET_UPLOAD_PROGRESS({ fileId, loaded, total, currentTime }));
- })
- .then(collection => {
- dispatch(collectionCreateActions.CREATE_COLLECTION_SUCCESS(collection));
- dispatch(reset('collectionCreateDialog'));
- dispatch(collectionUploaderActions.CLEAR_UPLOAD());
- });
- return collection;
- });
- };
-
-export type CollectionCreateAction = UnionOf<typeof collectionCreateActions>;
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { collectionCreatorReducer } from "./collection-creator-reducer";
-import { collectionCreateActions } from "./collection-creator-action";
-
-describe('collection-reducer', () => {
-
- it('should open collection creator dialog', () => {
- const initialState = { opened: false, ownerUuid: "" };
- const collection = { opened: true, ownerUuid: "" };
-
- const state = collectionCreatorReducer(initialState, collectionCreateActions.OPEN_COLLECTION_CREATOR(initialState));
- expect(state).toEqual(collection);
- });
-
- it('should close collection creator dialog', () => {
- const initialState = { opened: true, ownerUuid: "" };
- const collection = { opened: false, ownerUuid: "" };
-
- const state = collectionCreatorReducer(initialState, collectionCreateActions.CLOSE_COLLECTION_CREATOR());
- expect(state).toEqual(collection);
- });
-
- it('should reset collection creator dialog props', () => {
- const initialState = { opened: true, ownerUuid: "test" };
- const collection = { opened: false, ownerUuid: "" };
-
- const state = collectionCreatorReducer(initialState, collectionCreateActions.CREATE_COLLECTION_SUCCESS());
- expect(state).toEqual(collection);
- });
-});
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { collectionCreateActions, CollectionCreateAction } from './collection-creator-action';
-
-export interface CollectionCreatorState {
- opened: boolean;
- ownerUuid: string;
-}
-
-const updateCreator = (state: CollectionCreatorState, creator?: Partial<CollectionCreatorState>) => ({
- ...state,
- ...creator
-});
-
-const initialState: CollectionCreatorState = {
- opened: false,
- ownerUuid: ''
-};
-
-export const collectionCreatorReducer = (state: CollectionCreatorState = initialState, action: CollectionCreateAction) => {
- return collectionCreateActions.match(action, {
- OPEN_COLLECTION_CREATOR: ({ ownerUuid }) => updateCreator(state, { ownerUuid, opened: true }),
- CLOSE_COLLECTION_CREATOR: () => updateCreator(state, { opened: false }),
- CREATE_COLLECTION: () => updateCreator(state),
- CREATE_COLLECTION_SUCCESS: () => updateCreator(state, { opened: false, ownerUuid: "" }),
- default: () => state
- });
-};
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { default as unionize, ofType, UnionOf } from "unionize";
-import { Dispatch } from "redux";
-
-import { RootState } from "../../store";
-import { ServiceRepository } from "~/services/services";
-import { CollectionResource } from '~/models/collection';
-import { initialize } from 'redux-form';
-import { collectionPanelActions } from "../../collection-panel/collection-panel-action";
-import { ContextMenuResource } from "../../context-menu/context-menu-reducer";
-import { updateDetails } from "~/store/details-panel/details-panel-action";
-
-export const collectionUpdaterActions = unionize({
- OPEN_COLLECTION_UPDATER: ofType<{ uuid: string }>(),
- CLOSE_COLLECTION_UPDATER: ofType<{}>(),
- UPDATE_COLLECTION_SUCCESS: ofType<{}>(),
-}, {
- tag: 'type',
- value: 'payload'
-});
-
-
-export const COLLECTION_FORM_NAME = 'collectionEditDialog';
-
-export const openUpdater = (item: ContextMenuResource) =>
- (dispatch: Dispatch, getState: () => RootState) => {
- if (item) {
- dispatch(collectionUpdaterActions.OPEN_COLLECTION_UPDATER({ uuid: item.uuid }));
- dispatch(initialize(COLLECTION_FORM_NAME, { name: item.name, description: item.description }));
- }
- };
-
-export const updateCollection = (collection: Partial<CollectionResource>) =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const { uuid } = getState().collections.updater;
- return services.collectionService
- .update(uuid, collection)
- .then(collection => {
- dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: collection as CollectionResource }));
- dispatch(collectionUpdaterActions.UPDATE_COLLECTION_SUCCESS());
- dispatch<any>(updateDetails(collection));
- }
- );
- };
-
-export type CollectionUpdaterAction = UnionOf<typeof collectionUpdaterActions>;
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { collectionUpdaterActions, CollectionUpdaterAction } from './collection-updater-action';
-
-export interface CollectionUpdaterState {
- opened: boolean;
- uuid: string;
-}
-
-const updateCollection = (state: CollectionUpdaterState, updater?: Partial<CollectionUpdaterState>) => ({
- ...state,
- ...updater
-});
-
-const initialState: CollectionUpdaterState = {
- opened: false,
- uuid: ''
-};
-
-export const collectionUpdaterReducer = (state: CollectionUpdaterState = initialState, action: CollectionUpdaterAction) => {
- return collectionUpdaterActions.match(action, {
- OPEN_COLLECTION_UPDATER: ({ uuid }) => updateCollection(state, { uuid, opened: true }),
- CLOSE_COLLECTION_UPDATER: () => updateCollection(state, { opened: false, uuid: "" }),
- UPDATE_COLLECTION_SUCCESS: () => updateCollection(state, { opened: false, uuid: "" }),
- default: () => state
- });
-};
//
// SPDX-License-Identifier: AGPL-3.0
-import { default as unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from '~/common/unionize';
import { ContextMenuPosition, ContextMenuResource } from "./context-menu-reducer";
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { getResource } from '../resources/resources';
+import { ProjectResource } from '~/models/project';
+import { UserResource } from '~/models/user';
+import { isSidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
+import { extractUuidKind, ResourceKind } from '~/models/resource';
export const contextMenuActions = unionize({
OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
CLOSE_CONTEXT_MENU: ofType<{}>()
-}, {
- tag: 'type',
- value: 'payload'
- });
+});
export type ContextMenuAction = UnionOf<typeof contextMenuActions>;
+
+export type ContextMenuResource = {
+ name: string;
+ uuid: string;
+ ownerUuid: string;
+ description?: string;
+ kind: ContextMenuKind;
+ isTrashed?: boolean;
+}
+
+export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource) =>
+ (dispatch: Dispatch) => {
+ event.preventDefault();
+ dispatch(
+ contextMenuActions.OPEN_CONTEXT_MENU({
+ position: { x: event.clientX, y: event.clientY },
+ resource
+ })
+ );
+ };
+
+export const openRootProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const res = getResource<UserResource>(projectUuid)(getState().resources);
+ if (res) {
+ dispatch<any>(openContextMenu(event, {
+ name: '',
+ uuid: res.uuid,
+ ownerUuid: res.uuid,
+ kind: ContextMenuKind.ROOT_PROJECT,
+ isTrashed: false
+ }));
+ }
+ };
+
+export const openProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const res = getResource<ProjectResource>(projectUuid)(getState().resources);
+ if (res) {
+ dispatch<any>(openContextMenu(event, {
+ name: res.name,
+ uuid: res.uuid,
+ kind: ContextMenuKind.PROJECT,
+ ownerUuid: res.ownerUuid,
+ isTrashed: res.isTrashed
+ }));
+ }
+ };
+
+export const openSidePanelContextMenu = (event: React.MouseEvent<HTMLElement>, id: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ if (!isSidePanelTreeCategory(id)) {
+ const kind = extractUuidKind(id);
+ if (kind === ResourceKind.USER) {
+ dispatch<any>(openRootProjectContextMenu(event, id));
+ } else if (kind === ResourceKind.PROJECT) {
+ dispatch<any>(openProjectContextMenu(event, id));
+ }
+ }
+ };
+
+export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const resource = {
+ uuid: '',
+ name: '',
+ description: '',
+ kind: ContextMenuKind.PROCESS
+ };
+ dispatch<any>(openContextMenu(event, resource));
+ };
+
+export const resourceKindToContextMenuKind = (uuid: string) => {
+ const kind = extractUuidKind(uuid);
+ switch (kind) {
+ case ResourceKind.PROJECT:
+ return ContextMenuKind.PROJECT;
+ case ResourceKind.COLLECTION:
+ return ContextMenuKind.COLLECTION_RESOURCE;
+ case ResourceKind.USER:
+ return ContextMenuKind.ROOT_PROJECT;
+ default:
+ return;
+ }
+};
//
// SPDX-License-Identifier: AGPL-3.0
-import { default as unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters";
import { DataColumns } from "~/components/data-table/data-table";
TOGGLE_COLUMN: ofType<{ id: string, columnName: string }>(),
TOGGLE_SORT: ofType<{ id: string, columnName: string }>(),
SET_SEARCH_VALUE: ofType<{ id: string, searchValue: string }>(),
-}, { tag: "type", value: "payload" });
+});
export type DataExplorerAction = UnionOf<typeof dataExplorerActions>;
import { RootState } from "../store";
import { DataColumns } from "~/components/data-table/data-table";
import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters";
+import { DataExplorer } from './data-explorer-reducer';
+import { ListArguments, ListResults } from '~/common/api/common-resource-service';
export abstract class DataExplorerMiddlewareService {
protected readonly id: string;
abstract requestItems(api: MiddlewareAPI<Dispatch, RootState>): void;
}
+
+export const getDataExplorerColumnFilters = <T, F extends DataTableFilterItem>(columns: DataColumns<T, F>, columnName: string): F[] => {
+ const column = columns.find(c => c.name === columnName);
+ return column ? column.filters.filter(f => f.selected) : [];
+};
+
+export const dataExplorerToListParams = <R>(dataExplorer: DataExplorer) => ({
+ limit: dataExplorer.rowsPerPage,
+ offset: dataExplorer.page * dataExplorer.rowsPerPage,
+});
+
+export const listResultsToDataExplorerItemsMeta = <R>({ itemsAvailable, offset, limit }: ListResults<R>) => ({
+ itemsAvailable,
+ page: Math.floor(offset / limit),
+ rowsPerPage: limit
+});
\ No newline at end of file
filters: [],
render: jest.fn(),
selected: true,
- configurable: true,
- sortDirection: SortDirection.ASC
+ sortDirection: SortDirection.ASC,
+ configurable: true
}, {
name: "Column 2",
filters: [],
//
// SPDX-License-Identifier: AGPL-3.0
-import { unionize, ofType, UnionOf } from "unionize";
-import { Dispatch } from "redux";
-import { Resource, ResourceKind } from "~/models/resource";
-import { RootState } from "../store";
-import { ServiceRepository } from "~/services/services";
+import { unionize, ofType, UnionOf } from '~/common/unionize';
export const detailsPanelActions = unionize({
TOGGLE_DETAILS_PANEL: ofType<{}>(),
- LOAD_DETAILS: ofType<{ uuid: string, kind: ResourceKind }>(),
- LOAD_DETAILS_SUCCESS: ofType<{ item: Resource }>(),
- UPDATE_DETAILS: ofType<{ item: Resource }>()
-}, { tag: 'type', value: 'payload' });
+ LOAD_DETAILS_PANEL: ofType<string>()
+});
export type DetailsPanelAction = UnionOf<typeof detailsPanelActions>;
-export const loadDetails = (uuid: string, kind: ResourceKind) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch(detailsPanelActions.LOAD_DETAILS({ uuid, kind }));
- const item = await getService(services, kind).get(uuid);
- dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item }));
- };
-
-export const updateDetails = (item: Resource) =>
- async (dispatch: Dispatch, getState: () => RootState) => {
- const currentItem = getState().detailsPanel.item;
- if (currentItem && (currentItem.uuid === item.uuid)) {
- dispatch(detailsPanelActions.UPDATE_DETAILS({ item }));
- dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item }));
- }
- };
-
-
-const getService = (services: ServiceRepository, kind: ResourceKind) => {
- switch (kind) {
- case ResourceKind.PROJECT:
- return services.projectService;
- case ResourceKind.COLLECTION:
- return services.collectionService;
- default:
- return services.projectService;
- }
-};
+export const loadDetailsPanel = (uuid: string) => detailsPanelActions.LOAD_DETAILS_PANEL(uuid);
+
// SPDX-License-Identifier: AGPL-3.0
import { detailsPanelActions, DetailsPanelAction } from "./details-panel-action";
-import { Resource } from "~/models/resource";
export interface DetailsPanelState {
- item: Resource | null;
+ resourceUuid: string;
isOpened: boolean;
}
const initialState = {
- item: null,
+ resourceUuid: '',
isOpened: false
};
export const detailsPanelReducer = (state: DetailsPanelState = initialState, action: DetailsPanelAction) =>
detailsPanelActions.match(action, {
default: () => state,
- LOAD_DETAILS_SUCCESS: ({ item }) => ({ ...state, item }),
+ LOAD_DETAILS_PANEL: resourceUuid => ({ ...state, resourceUuid }),
TOGGLE_DETAILS_PANEL: () => ({ ...state, isOpened: !state.isOpened })
});
//
// SPDX-License-Identifier: AGPL-3.0
-import { default as unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
export const dialogActions = unionize({
OPEN_DIALOG: ofType<{ id: string, data: any }>(),
CLOSE_DIALOG: ofType<{ id: string }>()
-}, {
- tag: 'type',
- value: 'payload'
- });
+});
export type DialogAction = UnionOf<typeof dialogActions>;
import { DialogAction, dialogActions } from "./dialog-actions";
-export type DialogState = Record<string, Dialog>;
+export type DialogState = Record<string, Dialog<any>>;
-export interface Dialog {
+export interface Dialog <T> {
open: boolean;
- data: any;
+ data: T;
}
export const dialogReducer = (state: DialogState = {}, action: DialogAction) =>
default: () => state,
});
+export const getDialog = <T>(state: DialogState, id: string) =>
+ state[id] ? state[id] as Dialog<T> : undefined;
export const FAVORITE_PANEL_ID = "favoritePanel";
export const favoritePanelActions = bindDataExplorerActions(FAVORITE_PANEL_ID);
+
+export const loadFavoritePanel = () => favoritePanelActions.REQUEST_ITEMS();
import { FavoritePanelColumnNames, FavoritePanelFilter } from "~/views/favorite-panel/favorite-panel";
import { RootState } from "../store";
import { DataColumns } from "~/components/data-table/data-table";
-import { FavoritePanelItem, resourceToDataItem } from "~/views/favorite-panel/favorite-panel-item";
import { ServiceRepository } from "~/services/services";
import { SortDirection } from "~/components/data-table/data-column";
import { FilterBuilder } from "~/common/api/filter-builder";
-import { checkPresenceInFavorites } from "../favorites/favorites-actions";
+import { updateFavorites } from "../favorites/favorites-actions";
import { favoritePanelActions } from "./favorite-panel-action";
import { Dispatch, MiddlewareAPI } from "redux";
import { OrderBuilder, OrderDirection } from "~/common/api/order-builder";
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 { getDataExplorer } from "~/store/data-explorer/data-explorer-reducer";
export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
}
requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
- const dataExplorer = api.getState().dataExplorer[this.getId()];
- const columns = dataExplorer.columns as DataColumns<FavoritePanelItem, FavoritePanelFilter>;
- const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
- const typeFilters = this.getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
+ const dataExplorer = getDataExplorer(api.getState().dataExplorer, this.getId());
+ if (!dataExplorer) {
+ api.dispatch(favoritesPanelDataExplorerIsNotSet());
+ } else {
- const linkOrder = new OrderBuilder<LinkResource>();
- const contentOrder = new OrderBuilder<GroupContentsResource>();
+ 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);
- if (sortColumn && sortColumn.name === FavoritePanelColumnNames.NAME) {
- const direction = sortColumn.sortDirection === SortDirection.ASC
- ? OrderDirection.ASC
- : OrderDirection.DESC;
+ const linkOrder = new OrderBuilder<LinkResource>();
+ const contentOrder = new OrderBuilder<GroupContentsResource>();
- linkOrder.addOrder(direction, "name");
- contentOrder
- .addOrder(direction, "name", GroupContentsResourcePrefix.COLLECTION)
- .addOrder(direction, "name", GroupContentsResourcePrefix.PROCESS)
- .addOrder(direction, "name", GroupContentsResourcePrefix.PROJECT);
- }
+ if (sortColumn && sortColumn.name === FavoritePanelColumnNames.NAME) {
+ const direction = sortColumn.sortDirection === SortDirection.ASC
+ ? OrderDirection.ASC
+ : OrderDirection.DESC;
+
+ linkOrder.addOrder(direction, "name");
+ contentOrder
+ .addOrder(direction, "name", GroupContentsResourcePrefix.COLLECTION)
+ .addOrder(direction, "name", GroupContentsResourcePrefix.PROCESS)
+ .addOrder(direction, "name", GroupContentsResourcePrefix.PROJECT);
+ }
- this.services.favoriteService
- .list(this.services.authService.getUuid()!, {
- limit: dataExplorer.rowsPerPage,
- offset: dataExplorer.page * dataExplorer.rowsPerPage,
- linkOrder: linkOrder.getOrder(),
- contentOrder: contentOrder.getOrder(),
- filters: new FilterBuilder()
- .addIsA("headUuid", typeFilters.map(filter => filter.type))
- .addILike("name", dataExplorer.searchValue)
- .getFilters()
- })
- .then(response => {
- api.dispatch(favoritePanelActions.SET_ITEMS({
- items: response.items.map(resourceToDataItem),
- itemsAvailable: response.itemsAvailable,
- page: Math.floor(response.offset / response.limit),
- rowsPerPage: response.limit
- }));
- api.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
- })
- .catch(() => {
- api.dispatch(favoritePanelActions.SET_ITEMS({
- items: [],
- itemsAvailable: 0,
- page: 0,
- rowsPerPage: dataExplorer.rowsPerPage
- }));
- });
+ this.services.favoriteService
+ .list(this.services.authService.getUuid()!, {
+ limit: dataExplorer.rowsPerPage,
+ offset: dataExplorer.page * dataExplorer.rowsPerPage,
+ linkOrder: linkOrder.getOrder(),
+ contentOrder: contentOrder.getOrder(),
+ filters: new FilterBuilder()
+ .addIsA("headUuid", typeFilters.map(filter => filter.type))
+ .addILike("name", dataExplorer.searchValue)
+ .getFilters()
+ })
+ .then(response => {
+ api.dispatch(resourcesActions.SET_RESOURCES(response.items));
+ api.dispatch(favoritePanelActions.SET_ITEMS({
+ items: response.items.map(resource => resource.uuid),
+ itemsAvailable: response.itemsAvailable,
+ page: Math.floor(response.offset / response.limit),
+ rowsPerPage: response.limit
+ }));
+ api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
+ })
+ .catch(() => {
+ api.dispatch(favoritePanelActions.SET_ITEMS({
+ items: [],
+ itemsAvailable: 0,
+ page: 0,
+ rowsPerPage: dataExplorer.rowsPerPage
+ }));
+ });
+ }
}
}
+
+const favoritesPanelDataExplorerIsNotSet = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Favorites panel is not ready.'
+ });
//
// SPDX-License-Identifier: AGPL-3.0
-import { unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
import { Dispatch } from "redux";
import { RootState } from "../store";
import { checkFavorite } from "./favorites-reducer";
TOGGLE_FAVORITE: ofType<{ resourceUuid: string }>(),
CHECK_PRESENCE_IN_FAVORITES: ofType<string[]>(),
UPDATE_FAVORITES: ofType<Record<string, boolean>>()
-}, { tag: 'type', value: 'payload' });
+});
export type FavoritesAction = UnionOf<typeof favoritesActions>;
});
};
-export const checkPresenceInFavorites = (resourceUuids: string[]) =>
+export const updateFavorites = (resourceUuids: string[]) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const userUuid = getState().auth.user!.uuid;
dispatch(favoritesActions.CHECK_PRESENCE_IN_FAVORITES(resourceUuids));
dispatch(favoritesActions.UPDATE_FAVORITES(results));
});
};
-
-// Copyright (C) The Arvados Authors. All rights reserved.\r
-//\r
-// SPDX-License-Identifier: AGPL-3.0\r
-\r
-import { default as unionize, ofType, UnionOf } from "unionize";\r
-\r
-export interface UploadFile {\r
- id: number;\r
- file: File;\r
- prevLoaded: number;\r
- loaded: number;\r
- total: number;\r
- startTime: number;\r
- prevTime: number;\r
- currentTime: number;\r
-}\r
-\r
-export const collectionUploaderActions = unionize({\r
- SET_UPLOAD_FILES: ofType<File[]>(),\r
- START_UPLOAD: ofType(),\r
- SET_UPLOAD_PROGRESS: ofType<{ fileId: number, loaded: number, total: number, currentTime: number }>(),\r
- CLEAR_UPLOAD: ofType()\r
-}, {\r
- tag: 'type',\r
- value: 'payload'\r
-});\r
-\r
-export type CollectionUploaderAction = UnionOf<typeof collectionUploaderActions>;\r
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { unionize, ofType, UnionOf } from "~/common/unionize";
+
+export interface UploadFile {
+ id: number;
+ file: File;
+ prevLoaded: number;
+ loaded: number;
+ total: number;
+ startTime: number;
+ prevTime: number;
+ currentTime: number;
+}
+
+export const fileUploaderActions = unionize({
+ CLEAR_UPLOAD: ofType(),
+ SET_UPLOAD_FILES: ofType<File[]>(),
+ SET_UPLOAD_PROGRESS: ofType<{ fileId: number, loaded: number, total: number, currentTime: number }>(),
+ START_UPLOAD: ofType(),
+});
+
+export type FileUploaderAction = UnionOf<typeof fileUploaderActions>;
//
// SPDX-License-Identifier: AGPL-3.0
-import { CollectionUploaderAction, collectionUploaderActions, UploadFile } from "./collection-uploader-actions";
+import { UploadFile, fileUploaderActions, FileUploaderAction } from "./file-uploader-actions";
-export type CollectionUploaderState = UploadFile[];
+export type UploaderState = UploadFile[];
-const initialState: CollectionUploaderState = [];
+const initialState: UploaderState = [];
-export const collectionUploaderReducer = (state: CollectionUploaderState = initialState, action: CollectionUploaderAction) => {
- return collectionUploaderActions.match(action, {
+export const fileUploaderReducer = (state: UploaderState = initialState, action: FileUploaderAction) => {
+ return fileUploaderActions.match(action, {
SET_UPLOAD_FILES: files => files.map((f, idx) => ({
id: idx,
file: f,
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export interface MoveToFormDialogData {
+ name: string;
+ uuid: string;
+ ownerUuid: string;
+}
\ No newline at end of file
//
// SPDX-License-Identifier: AGPL-3.0
-import { Dispatch } from "redux";
-import { getProjectList, projectActions } from "../project/project-action";
+import { Dispatch, compose } from 'redux';
import { push } from "react-router-redux";
-import { TreeItemStatus } from "~/components/tree/tree";
-import { findTreeItem } from "../project/project-reducer";
-import { RootState } from "../store";
-import { ResourceKind } from "~/models/resource";
-import { projectPanelActions } from "../project-panel/project-panel-action";
+import { ResourceKind, extractUuidKind } from '~/models/resource';
import { getCollectionUrl } from "~/models/collection";
-import { getProjectUrl, ProjectResource } from "~/models/project";
-import { ProjectService } from "~/services/project-service/project-service";
-import { ServiceRepository } from "~/services/services";
-import { sidePanelActions } from "../side-panel/side-panel-action";
-import { SidePanelId } from "../side-panel/side-panel-reducer";
-import { getUuidObjectType, ObjectTypes } from "~/models/object-types";
-
-export const getResourceUrl = (resourceKind: ResourceKind, resourceUuid: string): string => {
- switch (resourceKind) {
- case ResourceKind.PROJECT: return getProjectUrl(resourceUuid);
- case ResourceKind.COLLECTION: return getCollectionUrl(resourceUuid);
- default:
- return '';
- }
-};
-
-export enum ItemMode {
- BOTH,
- OPEN,
- ACTIVE
-}
-
-export const setProjectItem = (itemId: string, itemMode: ItemMode) =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const { projects, router } = getState();
- const treeItem = findTreeItem(projects.items, itemId);
-
- if (treeItem) {
- const resourceUrl = getResourceUrl(treeItem.data.kind, treeItem.data.uuid);
-
- if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) {
- if (router.location && !router.location.pathname.includes(resourceUrl)) {
- dispatch(push(resourceUrl));
- }
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE({ itemId: treeItem.data.uuid }));
- }
-
- const promise = treeItem.status === TreeItemStatus.LOADED
- ? Promise.resolve()
- : dispatch<any>(getProjectList(itemId));
-
- promise
- .then(() => dispatch<any>(() => {
- if (itemMode === ItemMode.OPEN || itemMode === ItemMode.BOTH) {
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN({ itemId: treeItem.data.uuid }));
- }
- dispatch(projectPanelActions.RESET_PAGINATION());
- dispatch(projectPanelActions.REQUEST_ITEMS());
- }));
- } else {
- const uuid = services.authService.getUuid();
- if (itemId === uuid) {
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE({ itemId: uuid }));
- dispatch(projectPanelActions.RESET_PAGINATION());
- dispatch(projectPanelActions.REQUEST_ITEMS());
- }
+import { getProjectUrl } from "~/models/project";
+
+import { SidePanelTreeCategory } from '../side-panel-tree/side-panel-tree-actions';
+import { Routes, getProcessUrl } from '~/routes/routes';
+
+export const navigateTo = (uuid: string) =>
+ async (dispatch: Dispatch) => {
+ const kind = extractUuidKind(uuid);
+ if (kind === ResourceKind.PROJECT || kind === ResourceKind.USER) {
+ dispatch<any>(navigateToProject(uuid));
+ } else if (kind === ResourceKind.COLLECTION) {
+ dispatch<any>(navigateToCollection(uuid));
+ } else if (kind === ResourceKind.CONTAINER_REQUEST) {
+ dispatch<any>(navigateToProcess(uuid));
+ }
+ if (uuid === SidePanelTreeCategory.FAVORITES) {
+ dispatch<any>(navigateToFavorites);
}
};
-export const restoreBranch = (itemId: string) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const ancestors = await loadProjectAncestors(itemId, services.projectService);
- const uuids = ancestors.map(ancestor => ancestor.uuid);
- await loadBranch(uuids, dispatch);
- dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(SidePanelId.PROJECTS));
- uuids.forEach(uuid => {
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN({ itemId: uuid }));
- });
- };
+export const navigateToFavorites = push(Routes.FAVORITES);
+
+export const navigateToProject = compose(push, getProjectUrl);
-export const loadProjectAncestors = async (uuid: string, projectService: ProjectService): Promise<Array<ProjectResource>> => {
- if (getUuidObjectType(uuid) === ObjectTypes.USER) {
- return [];
- } else {
- const currentProject = await projectService.get(uuid);
- const ancestors = await loadProjectAncestors(currentProject.ownerUuid, projectService);
- return [...ancestors, currentProject];
- }
-};
+export const navigateToCollection = compose(push, getCollectionUrl);
-const loadBranch = async (uuids: string[], dispatch: Dispatch): Promise<any> => {
- const [uuid, ...rest] = uuids;
- if (uuid) {
- await dispatch<any>(getProjectList(uuid));
- return loadBranch(rest, dispatch);
- }
-};
+export const navigateToProcess = compose(push, getProcessUrl);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContainerRequestResource } from '../../models/container-request';
+import { ContainerResource } from '../../models/container';
+import { ResourcesState, getResource } from '~/store/resources/resources';
+import { filterResources } from '../resources/resources';
+import { ResourceKind, Resource } from '~/models/resource';
+
+export interface Process {
+ containerRequest: ContainerRequestResource;
+ container?: ContainerResource;
+}
+
+export const getProcess = (uuid: string) => (resources: ResourcesState): Process | undefined => {
+ const containerRequest = getResource<ContainerRequestResource>(uuid)(resources);
+ if (containerRequest) {
+ if (containerRequest.containerUuid) {
+ const container = getResource<ContainerResource>(containerRequest.containerUuid)(resources);
+ if (container) {
+ return { containerRequest, container };
+ }
+ }
+ return { containerRequest };
+ }
+ return;
+};
+
+export const getSubprocesses = (uuid: string) => (resources: ResourcesState) => {
+ const containerRequests = filterResources(isSubprocess(uuid))(resources) as ContainerRequestResource[];
+ return containerRequests.reduce((subprocesses, { uuid }) => {
+ const process = getProcess(uuid)(resources);
+ return process
+ ? [...subprocesses, process]
+ : subprocesses;
+ }, []);
+};
+
+export const getProcessStatus = (process: Process) =>
+ process.container
+ ? process.container.state
+ : process.containerRequest.state;
+
+const isSubprocess = (uuid: string) => (resource: Resource) =>
+ resource.kind === ResourceKind.CONTAINER_REQUEST
+ && (resource as ContainerRequestResource).requestingContainerUuid === uuid;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { RootState } from '~/store/store';
+import { ServiceRepository } from '~/services/services';
+import { updateResources } from '~/store/resources/resources-actions';
+import { FilterBuilder } from '~/common/api/filter-builder';
+import { ContainerRequestResource } from '../../models/container-request';
+
+export const loadProcess = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const containerRequest = await services.containerRequestService.get(uuid);
+ dispatch<any>(updateResources([containerRequest]));
+ if (containerRequest.containerUuid) {
+ const container = await services.containerService.get(containerRequest.containerUuid);
+ dispatch<any>(updateResources([container]));
+ }
+ };
+
+export const loadSubprocesses = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const containerRequests = await dispatch<any>(loadContainerRequests(
+ new FilterBuilder().addEqual('requestingContainerUuid', uuid).getFilters()
+ )) as ContainerRequestResource[];
+
+ const containerUuids: string[] = containerRequests.reduce((uuids, { containerUuid }) =>
+ containerUuid
+ ? [...uuids, containerUuid]
+ : uuids, []);
+
+ if (containerUuids.length > 0) {
+ await dispatch<any>(loadContainers(
+ new FilterBuilder().addIn('uuid', containerUuids).getFilters()
+ ));
+ }
+ };
+
+export const loadContainerRequests = (filters: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { items } = await services.containerRequestService.list({ filters });
+ dispatch<any>(updateResources(items));
+ return items;
+ };
+
+export const loadContainers = (filters: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { items } = await services.containerService.list({ filters });
+ dispatch<any>(updateResources(items));
+ return items;
+ };
// SPDX-License-Identifier: AGPL-3.0
import { bindDataExplorerActions } from "../data-explorer/data-explorer-action";
-
+import { propertiesActions } from "~/store/properties/properties-actions";
+import { Dispatch } from 'redux';
+import { ServiceRepository } from "~/services/services";
+import { RootState } from '~/store/store';
+import { getProperty } from "~/store/properties/properties";
export const PROJECT_PANEL_ID = "projectPanel";
+export const PROJECT_PANEL_CURRENT_UUID = "projectPanelCurrentUuid";
export const projectPanelActions = bindDataExplorerActions(PROJECT_PANEL_ID);
+
+export const openProjectPanel = (projectUuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(propertiesActions.SET_PROPERTY({ key: PROJECT_PANEL_CURRENT_UUID, value: projectUuid }));
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+ };
+
+export const getProjectPanelCurrentUuid = (state: RootState) => getProperty<string>(PROJECT_PANEL_CURRENT_UUID)(state.properties);
+
//
// SPDX-License-Identifier: AGPL-3.0
-import { DataExplorerMiddlewareService } from "../data-explorer/data-explorer-middleware-service";
+import { DataExplorerMiddlewareService, getDataExplorerColumnFilters, dataExplorerToListParams, listResultsToDataExplorerItemsMeta } from '../data-explorer/data-explorer-middleware-service';
import { ProjectPanelColumnNames, ProjectPanelFilter } from "~/views/project-panel/project-panel";
import { RootState } from "../store";
import { DataColumns } from "~/components/data-table/data-table";
import { ServiceRepository } from "~/services/services";
-import { ProjectPanelItem, resourceToDataItem } from "~/views/project-panel/project-panel-item";
import { SortDirection } from "~/components/data-table/data-column";
import { OrderBuilder, OrderDirection } from "~/common/api/order-builder";
import { FilterBuilder } from "~/common/api/filter-builder";
-import { GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
-import { checkPresenceInFavorites } from "../favorites/favorites-actions";
-import { projectPanelActions } from "./project-panel-action";
+import { GroupContentsResourcePrefix, GroupContentsResource } from "~/services/groups-service/groups-service";
+import { updateFavorites } from "../favorites/favorites-actions";
+import { projectPanelActions, PROJECT_PANEL_CURRENT_UUID } from './project-panel-action';
import { Dispatch, MiddlewareAPI } from "redux";
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 { DataExplorer, getDataExplorer } from '../data-explorer/data-explorer-reducer';
+import { ListResults } from '~/common/api/common-resource-service';
export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
super(id);
}
- requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+ async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
const state = api.getState();
- const dataExplorer = state.dataExplorer[this.getId()];
- const columns = dataExplorer.columns as DataColumns<ProjectPanelItem, ProjectPanelFilter>;
- const typeFilters = this.getColumnFilters(columns, ProjectPanelColumnNames.TYPE);
- const statusFilters = this.getColumnFilters(columns, ProjectPanelColumnNames.STATUS);
- const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+ const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
+ const projectUuid = getProperty<string>(PROJECT_PANEL_CURRENT_UUID)(state.properties);
+ if (!projectUuid) {
+ api.dispatch(projectPanelCurrentUuidIsNotSet());
+ } else if (!dataExplorer) {
+ api.dispatch(projectPanelDataExplorerIsNotSet());
+ } else {
+ try {
+ const response = await this.services.groupsService.contents(projectUuid, getParams(dataExplorer));
+ api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
+ api.dispatch(updateResources(response.items));
+ api.dispatch(setItems(response));
+ } catch (e) {
+ api.dispatch(couldNotFetchProjectContents());
+ }
+ }
+ }
+}
- const order = new OrderBuilder<ProjectResource>();
+const setItems = (listResults: ListResults<GroupContentsResource>) =>
+ projectPanelActions.SET_ITEMS({
+ ...listResultsToDataExplorerItemsMeta(listResults),
+ items: listResults.items.map(resource => resource.uuid),
+ });
- if (sortColumn) {
- const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
- ? OrderDirection.ASC
- : OrderDirection.DESC;
+const getParams = (dataExplorer: DataExplorer) => ({
+ ...dataExplorerToListParams(dataExplorer),
+ order: getOrder(dataExplorer),
+ filters: getFilters(dataExplorer),
+});
- const columnName = sortColumn && sortColumn.name === ProjectPanelColumnNames.NAME ? "name" : "createdAt";
- order
- .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.COLLECTION)
- .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROCESS)
- .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROJECT);
- }
+const getFilters = (dataExplorer: DataExplorer) => {
+ const columns = dataExplorer.columns as DataColumns<string, ProjectPanelFilter>;
+ const typeFilters = getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.TYPE);
+ const statusFilters = getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.STATUS);
+ return new FilterBuilder()
+ .addIsA("uuid", typeFilters.map(f => f.type))
+ .addIn("state", statusFilters.map(f => f.type), GroupContentsResourcePrefix.PROCESS)
+ .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
+ .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS)
+ .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
+ .getFilters();
+};
+
+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;
- this.services.groupsService
- .contents(state.projects.currentItemId, {
- limit: dataExplorer.rowsPerPage,
- offset: dataExplorer.page * dataExplorer.rowsPerPage,
- order: order.getOrder(),
- filters: new FilterBuilder()
- .addIsA("uuid", typeFilters.map(f => f.type))
- .addIn("state", statusFilters.map(f => f.type), GroupContentsResourcePrefix.PROCESS)
- .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
- .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS)
- .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
- .getFilters()
- })
- .then(response => {
- api.dispatch(projectPanelActions.SET_ITEMS({
- items: response.items.map(resourceToDataItem),
- itemsAvailable: response.itemsAvailable,
- page: Math.floor(response.offset / response.limit),
- rowsPerPage: response.limit
- }));
- api.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
- })
- .catch(() => {
- api.dispatch(projectPanelActions.SET_ITEMS({
- items: [],
- itemsAvailable: 0,
- page: 0,
- rowsPerPage: dataExplorer.rowsPerPage
- }));
- });
+ const columnName = sortColumn && sortColumn.name === ProjectPanelColumnNames.NAME ? "name" : "createdAt";
+ return order
+ .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.COLLECTION)
+ .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROCESS)
+ .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROJECT)
+ .getOrder();
+ } else {
+ return order.getOrder();
}
-}
+};
+
+const projectPanelCurrentUuidIsNotSet = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Project panel is not opened.'
+ });
+
+const couldNotFetchProjectContents = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Could not fetch project contents.'
+ });
+
+const projectPanelDataExplorerIsNotSet = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Project panel is not ready.'
+ });
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { RootState } from "~/store/store";
+import { ServiceRepository } from "~/services/services";
+import { TreePickerId, receiveTreePickerData } from "~/views-components/project-tree-picker/project-tree-picker";
+import { mockProjectResource } from "~/models/test-utils";
+import { treePickerActions } from "~/store/tree-picker/tree-picker-actions";
+
+export const resetPickerProjectTree = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch<any>(treePickerActions.RESET_TREE_PICKER({pickerId: TreePickerId.PROJECTS}));
+ dispatch<any>(treePickerActions.RESET_TREE_PICKER({pickerId: TreePickerId.SHARED_WITH_ME}));
+ dispatch<any>(treePickerActions.RESET_TREE_PICKER({pickerId: TreePickerId.FAVORITES}));
+
+ dispatch<any>(initPickerProjectTree());
+};
+
+export const initPickerProjectTree = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const uuid = services.authService.getUuid();
+
+ dispatch<any>(getPickerTreeProjects(uuid));
+ dispatch<any>(getSharedWithMeProjectsPickerTree(uuid));
+ dispatch<any>(getFavoritesProjectsPickerTree(uuid));
+};
+
+const getPickerTreeProjects = (uuid: string = '') => {
+ return getProjectsPickerTree(uuid, TreePickerId.PROJECTS);
+};
+
+const getSharedWithMeProjectsPickerTree = (uuid: string = '') => {
+ return getProjectsPickerTree(uuid, TreePickerId.SHARED_WITH_ME);
+};
+
+const getFavoritesProjectsPickerTree = (uuid: string = '') => {
+ return getProjectsPickerTree(uuid, TreePickerId.FAVORITES);
+};
+
+const getProjectsPickerTree = (uuid: string, kind: string) => {
+ return receiveTreePickerData(
+ '',
+ [mockProjectResource({ uuid, name: kind })],
+ kind
+ );
+};
\ No newline at end of file
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { projectsReducer, getTreePath } from "./project-reducer";
-import { projectActions } from "./project-action";
-import { TreeItem, TreeItemStatus } from "~/components/tree/tree";
-import { mockProjectResource } from "~/models/test-utils";
-
-describe('project-reducer', () => {
-
- it('should load projects', () => {
- const initialState = undefined;
-
- const projects = [mockProjectResource({ uuid: "1" }), mockProjectResource({ uuid: "2" })];
- const state = projectsReducer(initialState, projectActions.PROJECTS_SUCCESS({ projects, parentItemId: undefined }));
- expect(state).toEqual({
- items: [{
- active: false,
- open: false,
- id: "1",
- items: [],
- data: mockProjectResource({ uuid: "1" }),
- status: 0
- }, {
- active: false,
- open: false,
- id: "2",
- items: [],
- data: mockProjectResource({ uuid: "2" }),
- status: TreeItemStatus.INITIAL
- }
- ],
- currentItemId: "",
- creator: {
- opened: false,
- ownerUuid: "",
- },
- updater: {
- opened: false,
- uuid: ''
- }
- });
- });
-
- it('should remove activity on projects list', () => {
- const initialState = {
- items: [{
- data: mockProjectResource(),
- id: "1",
- open: true,
- active: true,
- status: TreeItemStatus.PENDING
- }],
- currentItemId: "1",
- creator: { opened: false, ownerUuid: "" },
- updater: { opened: false, uuid: '' }
- };
- const project = {
- items: [{
- data: mockProjectResource(),
- id: "1",
- open: true,
- active: false,
- status: TreeItemStatus.PENDING
- }],
- currentItemId: "",
- creator: { opened: false, ownerUuid: "" },
- updater: { opened: false, uuid: '' }
- };
-
- const state = projectsReducer(initialState, projectActions.RESET_PROJECT_TREE_ACTIVITY(initialState.items[0].id));
- expect(state).toEqual(project);
- });
-
- it('should toggle project tree item activity', () => {
- const initialState = {
- items: [{
- data: mockProjectResource(),
- id: "1",
- open: true,
- active: false,
- status: TreeItemStatus.PENDING
- }],
- currentItemId: "1",
- creator: { opened: false, ownerUuid: "" },
- updater: { opened: false, uuid: '' }
- };
- const project = {
- items: [{
- data: mockProjectResource(),
- id: "1",
- open: true,
- active: true,
- status: TreeItemStatus.PENDING,
- }],
- currentItemId: "1",
- creator: { opened: false, ownerUuid: "" },
- updater: { opened: false, uuid: '' }
- };
-
- const state = projectsReducer(initialState, projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE({ itemId: initialState.items[0].id }));
- expect(state).toEqual(project);
- });
-
-
- it('should close project tree item ', () => {
- const initialState = {
- items: [{
- data: mockProjectResource(),
- id: "1",
- open: true,
- active: false,
- status: TreeItemStatus.PENDING,
- }],
- currentItemId: "1",
- creator: { opened: false, ownerUuid: "" },
- updater: { opened: false, uuid: '' }
- };
- const project = {
- items: [{
- data: mockProjectResource(),
- id: "1",
- open: false,
- active: false,
- status: TreeItemStatus.PENDING,
- }],
- currentItemId: "1",
- creator: { opened: false, ownerUuid: "" },
- updater: { opened: false, uuid: '' }
-
- };
-
- const state = projectsReducer(initialState, projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN({ itemId: initialState.items[0].id }));
- expect(state).toEqual(project);
- });
-});
-
-describe("findTreeBranch", () => {
- const createTreeItem = (id: string, items?: Array<TreeItem<string>>): TreeItem<string> => ({
- id,
- items,
- active: false,
- data: "",
- open: false,
- status: TreeItemStatus.INITIAL
- });
-
- it("should return an array that matches path to the given item", () => {
- const tree: Array<TreeItem<string>> = [
- createTreeItem("1", [
- createTreeItem("1.1", [
- createTreeItem("1.1.1"),
- createTreeItem("1.1.2")
- ])
- ]),
- createTreeItem("2", [
- createTreeItem("2.1", [
- createTreeItem("2.1.1"),
- createTreeItem("2.1.2")
- ])
- ])
- ];
- const branch = getTreePath(tree, "2.1.1");
- expect(branch.map(item => item.id)).toEqual(["2", "2.1", "2.1.1"]);
- });
-
- it("should return empty array if item is not found", () => {
- const tree: Array<TreeItem<string>> = [
- createTreeItem("1", [
- createTreeItem("1.1", [
- createTreeItem("1.1.1"),
- createTreeItem("1.1.2")
- ])
- ]),
- createTreeItem("2", [
- createTreeItem("2.1", [
- createTreeItem("2.1.1"),
- createTreeItem("2.1.2")
- ])
- ])
- ];
- expect(getTreePath(tree, "3")).toHaveLength(0);
- });
-
-});
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { projectActions, ProjectAction } from "./project-action";
-import { TreeItem, TreeItemStatus } from "~/components/tree/tree";
-import { ProjectResource } from "~/models/project";
-
-export type ProjectState = {
- items: Array<TreeItem<ProjectResource>>,
- currentItemId: string,
- creator: ProjectCreator,
- updater: ProjectUpdater
-};
-
-interface ProjectCreator {
- opened: boolean;
- ownerUuid: string;
- error?: string;
-}
-
-interface ProjectUpdater {
- opened: boolean;
- uuid: string;
-}
-
-function rebuildTree<T>(tree: Array<TreeItem<T>>, action: (item: TreeItem<T>, visitedItems: TreeItem<T>[]) => void, visitedItems: TreeItem<T>[] = []): Array<TreeItem<T>> {
- const newTree: Array<TreeItem<T>> = [];
- for (const t of tree) {
- const items = t.items
- ? rebuildTree(t.items, action, visitedItems.concat(t))
- : undefined;
- const item: TreeItem<T> = { ...t, items };
- action(item, visitedItems);
- newTree.push(item);
- }
- return newTree;
-}
-
-export function findTreeItem<T>(tree: Array<TreeItem<T>>, itemId: string): TreeItem<T> | undefined {
- let item;
- for (const t of tree) {
- item = t.id === itemId
- ? t
- : findTreeItem(t.items ? t.items : [], itemId);
- if (item) {
- break;
- }
- }
- return item;
-}
-
-export function getTreePath<T>(tree: Array<TreeItem<T>>, itemId: string): Array<TreeItem<T>> {
- for (const item of tree) {
- if (item.id === itemId) {
- return [item];
- } else {
- const branch = getTreePath(item.items || [], itemId);
- if (branch.length > 0) {
- return [item, ...branch];
- }
- }
- }
- return [];
-}
-
-const updateCreator = (state: ProjectState, creator: Partial<ProjectCreator>) => ({
- ...state,
- creator: {
- ...state.creator,
- ...creator
- }
-});
-
-const updateProject = (state: ProjectState, updater?: Partial<ProjectUpdater>) => ({
- ...state,
- updater: {
- ...state.updater,
- ...updater
- }
-});
-
-const initialState: ProjectState = {
- items: [],
- currentItemId: "",
- creator: {
- opened: false,
- ownerUuid: ""
- },
- updater: {
- opened: false,
- uuid: ''
- }
-};
-
-export const projectsReducer = (state: ProjectState = initialState, action: ProjectAction) => {
- return projectActions.match(action, {
- OPEN_PROJECT_CREATOR: ({ ownerUuid }) => updateCreator(state, { ownerUuid, opened: true }),
- CLOSE_PROJECT_CREATOR: () => updateCreator(state, { opened: false }),
- CREATE_PROJECT: () => updateCreator(state, { error: undefined }),
- CREATE_PROJECT_SUCCESS: () => updateCreator(state, { opened: false, ownerUuid: "" }),
- OPEN_PROJECT_UPDATER: ({ uuid }) => updateProject(state, { uuid, opened: true }),
- CLOSE_PROJECT_UPDATER: () => updateProject(state, { opened: false, uuid: "" }),
- UPDATE_PROJECT_SUCCESS: () => updateProject(state, { opened: false, uuid: "" }),
- REMOVE_PROJECT: () => state,
- PROJECTS_REQUEST: itemId => {
- return {
- ...state,
- items: rebuildTree(state.items, item => {
- if (item.id === itemId) {
- item.status = TreeItemStatus.PENDING;
- }
- })
- };
- },
- PROJECTS_SUCCESS: ({ projects, parentItemId }) => {
- const items = projects.map(p => ({
- id: p.uuid,
- open: false,
- active: false,
- status: TreeItemStatus.INITIAL,
- data: p,
- items: []
- }));
- return {
- ...state,
- items: state.items.length > 0 ?
- rebuildTree(state.items, item => {
- if (item.id === parentItemId) {
- item.status = TreeItemStatus.LOADED;
- item.items = items;
- }
- }) : items
- };
- },
- TOGGLE_PROJECT_TREE_ITEM_OPEN: ({ itemId, open, recursive }) => ({
- ...state,
- items: rebuildTree(state.items, (item, visitedItems) => {
- if (item.id === itemId) {
- if (recursive && open !== undefined) {
- visitedItems.forEach(item => item.open = open);
- }
- item.open = open !== undefined ? open : !item.open;
- }
- }),
- currentItemId: itemId
- }),
- TOGGLE_PROJECT_TREE_ITEM_ACTIVE: ({ itemId, active, recursive }) => ({
- ...state,
- items: rebuildTree(state.items, (item, visitedItems) => {
- item.active = false;
- if (item.id === itemId) {
- if (recursive && active !== undefined) {
- visitedItems.forEach(item => item.active = active);
- }
-
- item.active = active !== undefined ? active : true;
- }
- }),
- currentItemId: itemId
- }),
- RESET_PROJECT_TREE_ACTIVITY: () => ({
- ...state,
- items: rebuildTree(state.items, item => {
- item.active = false;
- }),
- currentItemId: ""
- }),
- default: () => state
- });
-};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { reset, startSubmit, stopSubmit, initialize } from 'redux-form';
+import { RootState } from '~/store/store';
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { getCommonResourceServiceError, CommonResourceServiceError } from '~/common/api/common-resource-service';
+import { ProjectResource } from '~/models/project';
+import { ServiceRepository } from '~/services/services';
+
+export interface ProjectCreateFormDialogData {
+ ownerUuid: string;
+ name: string;
+ description: string;
+}
+
+export const PROJECT_CREATE_FORM_NAME = 'projectCreateFormName';
+
+export const openProjectCreateDialog = (ownerUuid: string) =>
+ (dispatch: Dispatch) => {
+ dispatch(initialize(PROJECT_CREATE_FORM_NAME, { ownerUuid }));
+ dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_CREATE_FORM_NAME, data: {} }));
+ };
+
+export const createProject = (project: Partial<ProjectResource>) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(startSubmit(PROJECT_CREATE_FORM_NAME));
+ try {
+ const newProject = await services.projectService.create(project);
+ dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_CREATE_FORM_NAME }));
+ dispatch(reset(PROJECT_CREATE_FORM_NAME));
+ return newProject;
+ } catch (e) {
+ const error = getCommonResourceServiceError(e);
+ if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+ dispatch(stopSubmit(PROJECT_CREATE_FORM_NAME, { name: 'Project with the same name already exists.' }));
+ }
+ return undefined;
+ }
+ };
--- /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 "~/common/api/common-resource-service";
+import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
+import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
+
+export const PROJECT_MOVE_FORM_NAME = 'projectMoveFormName';
+
+export const openMoveProjectDialog = (resource: { name: string, uuid: string }) =>
+ (dispatch: Dispatch) => {
+ dispatch<any>(resetPickerProjectTree());
+ dispatch(initialize(PROJECT_MOVE_FORM_NAME, resource));
+ dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_MOVE_FORM_NAME, data: {} }));
+ };
+
+export const moveProject = (resource: MoveToFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(startSubmit(PROJECT_MOVE_FORM_NAME));
+ try {
+ const project = await services.projectService.get(resource.uuid);
+ const newProject = await services.projectService.update(resource.uuid, { ...project, ownerUuid: resource.ownerUuid });
+ dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_MOVE_FORM_NAME }));
+ return newProject;
+ } catch (e) {
+ const error = getCommonResourceServiceError(e);
+ if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+ dispatch(stopSubmit(PROJECT_MOVE_FORM_NAME, { ownerUuid: 'A project with the same name already exists in the target project.' }));
+ } else if (error === CommonResourceServiceError.OWNERSHIP_CYCLE) {
+ dispatch(stopSubmit(PROJECT_MOVE_FORM_NAME, { ownerUuid: 'Cannot move a project into itself.' }));
+ } else {
+ dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_MOVE_FORM_NAME }));
+ throw new Error('Could not move the project.');
+ }
+ return;
+ }
+ };
--- /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 { ContextMenuResource } from '~/store/context-menu/context-menu-reducer';
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/common/api/common-resource-service";
+import { ServiceRepository } from "~/services/services";
+import { ProjectResource } from '~/models/project';
+
+export interface ProjectUpdateFormDialogData {
+ uuid: string;
+ name: string;
+ description: string;
+}
+
+export const PROJECT_UPDATE_FORM_NAME = 'projectUpdateFormName';
+
+export const openProjectUpdateDialog = (resource: ContextMenuResource) =>
+ (dispatch: Dispatch) => {
+ dispatch(initialize(PROJECT_UPDATE_FORM_NAME, resource));
+ dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_UPDATE_FORM_NAME, data: {} }));
+ };
+
+export const updateProject = (project: Partial<ProjectResource>) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const uuid = project.uuid || '';
+ dispatch(startSubmit(PROJECT_UPDATE_FORM_NAME));
+ try {
+ const updatedProject = await services.projectService.update(uuid, project);
+ dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_UPDATE_FORM_NAME }));
+ return updatedProject;
+ } catch (e) {
+ const error = getCommonResourceServiceError(e);
+ if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+ dispatch(stopSubmit(PROJECT_UPDATE_FORM_NAME, { name: 'Project with the same name already exists.' }));
+ }
+ return ;
+ }
+ };
\ No newline at end of file
--- /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 propertiesActions = unionize({
+ SET_PROPERTY: ofType<{ key: string, value: any }>(),
+ DELETE_PROPERTY: ofType<string>(),
+});
+
+export type PropertiesAction = UnionOf<typeof propertiesActions>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { PropertiesState, setProperty, deleteProperty } from './properties';
+import { PropertiesAction, propertiesActions } from './properties-actions';
+
+
+export const propertiesReducer = (state: PropertiesState = {}, action: PropertiesAction) =>
+ propertiesActions.match(action, {
+ SET_PROPERTY: ({ key, value }) => setProperty(key, value)(state),
+ DELETE_PROPERTY: key => deleteProperty(key)(state),
+ default: () => state,
+ });
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export type PropertiesState = { [key: string]: any };
+
+export const getProperty = <T>(id: string) =>
+ (state: PropertiesState): T | undefined =>
+ state[id];
+
+export const setProperty = <T>(id: string, data: T) =>
+ (state: PropertiesState) => ({
+ ...state,
+ [id]: data
+ });
+
+export const deleteProperty = (id: string) =>
+ (state: PropertiesState) => {
+ const newState = { ...state };
+ delete newState[id];
+ return newState;
+ };
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { unionize, ofType, UnionOf } from '~/common/unionize';
+import { Resource, extractUuidKind } from '~/models/resource';
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { ServiceRepository } from '~/services/services';
+import { getResourceService } from '~/services/services';
+
+export const resourcesActions = unionize({
+ SET_RESOURCES: ofType<Resource[]>(),
+ DELETE_RESOURCES: ofType<string[]>()
+});
+
+export type ResourcesAction = UnionOf<typeof resourcesActions>;
+
+export const updateResources = (resources: Resource[]) => resourcesActions.SET_RESOURCES(resources);
+
+export const loadResource = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const kind = extractUuidKind(uuid);
+ const service = getResourceService(kind)(services);
+ if (service) {
+ const resource = await service.get(uuid);
+ dispatch<any>(updateResources([resource]));
+ return resource;
+ }
+ return undefined;
+ };
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ResourcesState, setResource, deleteResource } from './resources';
+import { ResourcesAction, resourcesActions } from './resources-actions';
+
+export const resourcesReducer = (state: ResourcesState = {}, action: ResourcesAction) =>
+ resourcesActions.match(action, {
+ SET_RESOURCES: resources => resources.reduce((state, resource) => setResource(resource.uuid, resource)(state), state),
+ DELETE_RESOURCES: ids => ids.reduce((state, id) => deleteResource(id)(state), state),
+ default: () => state,
+ });
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Resource } from "~/models/resource";
+import { ResourceKind } from '../../models/resource';
+
+export type ResourcesState = { [key: string]: Resource };
+
+export const getResource = <T extends Resource = Resource>(id: string) =>
+ (state: ResourcesState): T | undefined =>
+ state[id] as T;
+
+export const setResource = <T extends Resource>(id: string, data: T) =>
+ (state: ResourcesState) => ({
+ ...state,
+ [id]: data
+ });
+
+export const deleteResource = (id: string) =>
+ (state: ResourcesState) => {
+ const newState = { ...state };
+ delete newState[id];
+ return newState;
+ };
+
+export const filterResources = (filter: (resource: Resource) => boolean) =>
+ (state: ResourcesState) =>
+ Object
+ .keys(state)
+ .reduce((resources, id) => {
+ const resource = getResource(id)(state);
+ return resource
+ ? [...resources, resource]
+ : resources;
+ }, [])
+ .filter(filter);
+
+export const filterResourcesByKind = (kind: ResourceKind) =>
+ (state: ResourcesState) =>
+ filterResources(resource => resource.kind === kind)(state);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from 'redux';
+import { treePickerActions } from "~/store/tree-picker/tree-picker-actions";
+import { createTreePickerNode, TreePickerNode } from '~/store/tree-picker/tree-picker';
+import { RootState } from '../store';
+import { ServiceRepository } from '~/services/services';
+import { FilterBuilder } from '~/common/api/filter-builder';
+import { resourcesActions } from '../resources/resources-actions';
+import { getTreePicker, TreePicker } from '../tree-picker/tree-picker';
+import { TreeItemStatus } from "~/components/tree/tree";
+import { getNodeAncestors, getNodeValue, getNodeAncestorsIds, getNode } from '~/models/tree';
+import { ProjectResource } from '~/models/project';
+
+export enum SidePanelTreeCategory {
+ PROJECTS = 'Projects',
+ SHARED_WITH_ME = 'Shared with me',
+ WORKFLOWS = 'Workflows',
+ RECENT_OPEN = 'Recent open',
+ FAVORITES = 'Favorites',
+ TRASH = 'Trash'
+}
+
+export const SIDE_PANEL_TREE = 'sidePanelTree';
+
+export const getSidePanelTree = (treePicker: TreePicker) =>
+ getTreePicker<ProjectResource | string>(SIDE_PANEL_TREE)(treePicker);
+
+export const getSidePanelTreeBranch = (uuid: string) => (treePicker: TreePicker): Array<TreePickerNode<ProjectResource | string>> => {
+ const tree = getSidePanelTree(treePicker);
+ if (tree) {
+ const ancestors = getNodeAncestors(uuid)(tree).map(node => node.value);
+ const node = getNodeValue(uuid)(tree);
+ if (node) {
+ return [...ancestors, node];
+ }
+ }
+ return [];
+};
+
+const SIDE_PANEL_CATEGORIES = [
+ SidePanelTreeCategory.SHARED_WITH_ME,
+ SidePanelTreeCategory.WORKFLOWS,
+ SidePanelTreeCategory.RECENT_OPEN,
+ SidePanelTreeCategory.FAVORITES,
+ SidePanelTreeCategory.TRASH,
+];
+
+export const isSidePanelTreeCategory = (id: string) => SIDE_PANEL_CATEGORIES.some(category => category === id);
+
+export const initSidePanelTree = () =>
+ (dispatch: Dispatch, getState: () => RootState, { authService }: ServiceRepository) => {
+ const rootProjectUuid = authService.getUuid() || '';
+ const nodes = SIDE_PANEL_CATEGORIES.map(nodeId => createTreePickerNode({ nodeId, value: nodeId }));
+ const projectsNode = createTreePickerNode({ nodeId: rootProjectUuid, value: SidePanelTreeCategory.PROJECTS });
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+ nodeId: '',
+ pickerId: SIDE_PANEL_TREE,
+ nodes: [projectsNode, ...nodes]
+ }));
+ SIDE_PANEL_CATEGORIES.forEach(category => {
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+ nodeId: category,
+ pickerId: SIDE_PANEL_TREE,
+ nodes: []
+ }));
+ });
+ };
+
+export const loadSidePanelTreeProjects = (projectUuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const treePicker = getTreePicker(SIDE_PANEL_TREE)(getState().treePicker);
+ const node = treePicker ? getNode(projectUuid)(treePicker) : undefined;
+ if (node || projectUuid === '') {
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId: projectUuid, pickerId: SIDE_PANEL_TREE }));
+ const params = {
+ filters: new FilterBuilder()
+ .addEqual('ownerUuid', projectUuid)
+ .getFilters()
+ };
+ const { items } = await services.projectService.list(params);
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+ nodeId: projectUuid,
+ pickerId: SIDE_PANEL_TREE,
+ nodes: items.map(item => createTreePickerNode({ nodeId: item.uuid, value: item })),
+ }));
+ dispatch(resourcesActions.SET_RESOURCES(items));
+ }
+ };
+
+export const activateSidePanelTreeItem = (nodeId: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const node = getSidePanelTreeNode(nodeId)(getState().treePicker);
+ if (node && !node.selected) {
+ dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, pickerId: SIDE_PANEL_TREE }));
+ }
+ if (!isSidePanelTreeCategory(nodeId)) {
+ await dispatch<any>(activateSidePanelTreeProject(nodeId));
+ }
+ };
+
+export const activateSidePanelTreeProject = (nodeId: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { treePicker } = getState();
+ const node = getSidePanelTreeNode(nodeId)(treePicker);
+ if (node && node.status !== TreeItemStatus.LOADED) {
+ await dispatch<any>(loadSidePanelTreeProjects(nodeId));
+ } else if (node === undefined) {
+ await dispatch<any>(activateSidePanelTreeBranch(nodeId));
+ }
+ dispatch(treePickerActions.EXPAND_TREE_PICKER_NODES({
+ nodeIds: getSidePanelTreeNodeAncestorsIds(nodeId)(treePicker),
+ pickerId: SIDE_PANEL_TREE
+ }));
+ dispatch<any>(expandSidePanelTreeItem(nodeId));
+ };
+
+export const activateSidePanelTreeBranch = (nodeId: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const ancestors = await services.ancestorsService.ancestors(nodeId, services.authService.getUuid() || '');
+ for (const ancestor of ancestors) {
+ await dispatch<any>(loadSidePanelTreeProjects(ancestor.uuid));
+ }
+ dispatch(treePickerActions.EXPAND_TREE_PICKER_NODES({
+ nodeIds: ancestors.map(ancestor => ancestor.uuid),
+ pickerId: SIDE_PANEL_TREE
+ }));
+ dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, pickerId: SIDE_PANEL_TREE }));
+ };
+
+export const toggleSidePanelTreeItemCollapse = (nodeId: string) =>
+ async (dispatch: Dispatch, getState: () => RootState) => {
+ const node = getSidePanelTreeNode(nodeId)(getState().treePicker);
+ if (node && node.status === TreeItemStatus.INITIAL) {
+ await dispatch<any>(loadSidePanelTreeProjects(node.nodeId));
+ }
+ dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId: SIDE_PANEL_TREE }));
+ };
+
+export const expandSidePanelTreeItem = (nodeId: string) =>
+ async (dispatch: Dispatch, getState: () => RootState) => {
+ const node = getSidePanelTreeNode(nodeId)(getState().treePicker);
+ if (node && node.collapsed) {
+ dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId: SIDE_PANEL_TREE }));
+ }
+ };
+
+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) => {
+ const sidePanelTree = getTreePicker(SIDE_PANEL_TREE)(treePicker);
+ return sidePanelTree
+ ? getNodeAncestorsIds(nodeId)(sidePanelTree)
+ : [];
+};
//
// SPDX-License-Identifier: AGPL-3.0
-import { default as unionize, ofType, UnionOf } from "unionize";
-import { SidePanelId } from "~/store/side-panel/side-panel-reducer";
+import { Dispatch } from 'redux';
+import { isSidePanelTreeCategory, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
+import { navigateToFavorites, navigateTo } from '../navigation/navigation-action';
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
-export const sidePanelActions = unionize({
- TOGGLE_SIDE_PANEL_ITEM_OPEN: ofType<SidePanelId>()
-}, {
- tag: 'type',
- value: 'payload'
-});
+export const navigateFromSidePanel = (id: string) =>
+ (dispatch: Dispatch) => {
+ if (isSidePanelTreeCategory(id)) {
+ dispatch<any>(getSidePanelTreeCategoryAction(id));
+ } else {
+ dispatch<any>(navigateTo(id));
+ }
+ };
-export type SidePanelAction = UnionOf<typeof sidePanelActions>;
+const getSidePanelTreeCategoryAction = (id: string) => {
+ switch (id) {
+ case SidePanelTreeCategory.FAVORITES:
+ return navigateToFavorites;
+ default:
+ return sidePanelTreeCategoryNotAvailable(id);
+ }
+};
+
+const sidePanelTreeCategoryNotAvailable = (id: string) =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: `${id} not available`,
+ hideDuration: 3000,
+ });
\ No newline at end of file
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { sidePanelReducer } from "./side-panel-reducer";
-import { sidePanelActions } from "./side-panel-action";
-import { ProjectsIcon } from "~/components/icon/icon";
-
-describe('side-panel-reducer', () => {
- it('should open side-panel item', () => {
- const initialState = [
- {
- id: "1",
- name: "Projects",
- url: "/projects",
- icon: ProjectsIcon,
- open: false
- }
- ];
- const project = [
- {
- id: "1",
- name: "Projects",
- icon: ProjectsIcon,
- open: true,
- url: "/projects"
- }
- ];
-
- const state = sidePanelReducer(initialState, sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(initialState[0].id));
- expect(state).toEqual(project);
- });
-});
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { sidePanelActions, SidePanelAction } from './side-panel-action';
-import { SidePanelItem } from '~/components/side-panel/side-panel';
-import { ProjectsIcon, ShareMeIcon, WorkflowIcon, RecentIcon, FavoriteIcon, TrashIcon } from "~/components/icon/icon";
-import { Dispatch } from "redux";
-import { push } from "react-router-redux";
-import { favoritePanelActions } from "../favorite-panel/favorite-panel-action";
-import { projectPanelActions } from "../project-panel/project-panel-action";
-import { projectActions } from "../project/project-action";
-import { getProjectUrl } from "~/models/project";
-import { columns as projectPanelColumns } from "~/views/project-panel/project-panel";
-import { columns as favoritePanelColumns } from "~/views/favorite-panel/favorite-panel";
-import { columns as trashPanelColumns } from "~/views/trash-panel/trash-panel";
-import { trashPanelActions } from "~/store/trash-panel/trash-panel-action";
-
-export type SidePanelState = SidePanelItem[];
-
-export const sidePanelReducer = (state: SidePanelState = sidePanelItems, action: SidePanelAction) => {
- return sidePanelActions.match(action, {
- TOGGLE_SIDE_PANEL_ITEM_OPEN: itemId =>
- state.map(it => ({...it, open: itemId === it.id && it.open === false})),
- default: () => state
- });
-};
-
-export enum SidePanelId {
- PROJECTS = "Projects",
- SHARED_WITH_ME = "SharedWithMe",
- WORKFLOWS = "Workflows",
- RECENT_OPEN = "RecentOpen",
- FAVORITES = "Favourites",
- TRASH = "Trash"
-}
-
-export const sidePanelItems = [
- {
- id: SidePanelId.PROJECTS,
- name: "Projects",
- url: "/projects",
- icon: ProjectsIcon,
- open: false,
- active: false,
- margin: true,
- openAble: true,
- activeAction: (dispatch: Dispatch, uuid: string) => {
- dispatch(push(getProjectUrl(uuid)));
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE({ itemId: uuid }));
- dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
- dispatch(projectPanelActions.RESET_PAGINATION());
- dispatch(projectPanelActions.REQUEST_ITEMS());
- }
- },
- {
- id: SidePanelId.SHARED_WITH_ME,
- name: "Shared with me",
- url: "/shared",
- icon: ShareMeIcon,
- active: false,
- activeAction: (dispatch: Dispatch) => {
- dispatch(push("/shared"));
- }
- },
- {
- id: SidePanelId.WORKFLOWS,
- name: "Workflows",
- url: "/workflows",
- icon: WorkflowIcon,
- active: false,
- activeAction: (dispatch: Dispatch) => {
- dispatch(push("/workflows"));
- }
- },
- {
- id: SidePanelId.RECENT_OPEN,
- name: "Recent open",
- url: "/recent",
- icon: RecentIcon,
- active: false,
- activeAction: (dispatch: Dispatch) => {
- dispatch(push("/recent"));
- }
- },
- {
- id: SidePanelId.FAVORITES,
- name: "Favorites",
- url: "/favorites",
- icon: FavoriteIcon,
- active: false,
- activeAction: (dispatch: Dispatch) => {
- dispatch(push("/favorites"));
- dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
- dispatch(favoritePanelActions.RESET_PAGINATION());
- dispatch(favoritePanelActions.REQUEST_ITEMS());
- }
- },
- {
- id: SidePanelId.TRASH,
- name: "Trash",
- url: "/trash",
- icon: TrashIcon,
- active: false,
- activeAction: (dispatch: Dispatch) => {
- dispatch(push("/trash"));
- dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
- dispatch(trashPanelActions.RESET_PAGINATION());
- dispatch(trashPanelActions.REQUEST_ITEMS());
- }
- }
-];
//
// SPDX-License-Identifier: AGPL-3.0
-import { unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
export const snackbarActions = unionize({
OPEN_SNACKBAR: ofType<{message: string; hideDuration?: number}>(),
CLOSE_SNACKBAR: ofType<{}>()
-}, { tag: 'type', value: 'payload' });
+});
export type SnackbarAction = UnionOf<typeof snackbarActions>;
export const snackbarReducer = (state = initialState, action: SnackbarAction) => {
return snackbarActions.match(action, {
- OPEN_SNACKBAR: data => ({ ...data, open: true }),
+ OPEN_SNACKBAR: data => ({ ...initialState, ...data, open: true }),
CLOSE_SNACKBAR: () => initialState,
default: () => state,
});
// SPDX-License-Identifier: AGPL-3.0
import { createStore, applyMiddleware, compose, Middleware, combineReducers, Store, Action, Dispatch } from 'redux';
-import { routerMiddleware, routerReducer, RouterState } from "react-router-redux";
+import { routerMiddleware, routerReducer } from "react-router-redux";
import thunkMiddleware from 'redux-thunk';
import { History } from "history";
-import { projectsReducer, ProjectState } from "./project/project-reducer";
-import { sidePanelReducer, SidePanelState } from './side-panel/side-panel-reducer';
-import { authReducer, AuthState } from "./auth/auth-reducer";
-import { dataExplorerReducer, DataExplorerState } from './data-explorer/data-explorer-reducer';
-import { detailsPanelReducer, DetailsPanelState } from './details-panel/details-panel-reducer';
-import { contextMenuReducer, ContextMenuState } from './context-menu/context-menu-reducer';
+import { authReducer } from "./auth/auth-reducer";
+import { dataExplorerReducer } from './data-explorer/data-explorer-reducer';
+import { detailsPanelReducer } from './details-panel/details-panel-reducer';
+import { contextMenuReducer } from './context-menu/context-menu-reducer';
import { reducer as formReducer } from 'redux-form';
-import { FavoritesState, favoritesReducer } from './favorites/favorites-reducer';
-import { snackbarReducer, SnackbarState } from './snackbar/snackbar-reducer';
-import { CollectionPanelFilesState } from './collection-panel/collection-panel-files/collection-panel-files-state';
+import { favoritesReducer } from './favorites/favorites-reducer';
+import { snackbarReducer } from './snackbar/snackbar-reducer';
import { collectionPanelFilesReducer } from './collection-panel/collection-panel-files/collection-panel-files-reducer';
import { dataExplorerMiddleware } from "./data-explorer/data-explorer-middleware";
import { FAVORITE_PANEL_ID } from "./favorite-panel/favorite-panel-action";
import { PROJECT_PANEL_ID } from "./project-panel/project-panel-action";
import { ProjectPanelMiddlewareService } from "./project-panel/project-panel-middleware-service";
import { FavoritePanelMiddlewareService } from "./favorite-panel/favorite-panel-middleware-service";
-import { CollectionPanelState, collectionPanelReducer } from './collection-panel/collection-panel-reducer';
-import { DialogState, dialogReducer } from './dialog/dialog-reducer';
-import { CollectionsState, collectionsReducer } from './collections/collections-reducer';
+import { collectionPanelReducer } from './collection-panel/collection-panel-reducer';
+import { dialogReducer } from './dialog/dialog-reducer';
import { ServiceRepository } from "~/services/services";
import { treePickerReducer } from './tree-picker/tree-picker-reducer';
-import { TreePicker } from './tree-picker/tree-picker';
-import { TrashPanelMiddlewareService } from "~/store/trash-panel/trash-panel-middleware-service";
-import { TRASH_PANEL_ID } from "~/store/trash-panel/trash-panel-action";
+import { resourcesReducer } from '~/store/resources/resources-reducer';
+import { propertiesReducer } from './properties/properties-reducer';
+import { RootState } from './store';
+import { fileUploaderReducer } from './file-uploader/file-uploader-reducer';
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
compose;
-export interface RootState {
- auth: AuthState;
- projects: ProjectState;
- collections: CollectionsState;
- router: RouterState;
- dataExplorer: DataExplorerState;
- sidePanel: SidePanelState;
- collectionPanel: CollectionPanelState;
- detailsPanel: DetailsPanelState;
- contextMenu: ContextMenuState;
- favorites: FavoritesState;
- snackbar: SnackbarState;
- collectionPanelFiles: CollectionPanelFilesState;
- dialog: DialogState;
- treePicker: TreePicker;
-}
+export type RootState = ReturnType<ReturnType<typeof createRootReducer>>;
export type RootStore = Store<RootState, Action> & { dispatch: Dispatch<any> };
export function configureStore(history: History, services: ServiceRepository): RootStore {
- const rootReducer = combineReducers({
- auth: authReducer(services),
- projects: projectsReducer,
- collections: collectionsReducer,
- router: routerReducer,
- dataExplorer: dataExplorerReducer,
- sidePanel: sidePanelReducer,
- collectionPanel: collectionPanelReducer,
- detailsPanel: detailsPanelReducer,
- contextMenu: contextMenuReducer,
- form: formReducer,
- favorites: favoritesReducer,
- snackbar: snackbarReducer,
- collectionPanelFiles: collectionPanelFilesReducer,
- dialog: dialogReducer,
- treePicker: treePickerReducer,
- });
+ const rootReducer = createRootReducer(services);
const projectPanelMiddleware = dataExplorerMiddleware(
new ProjectPanelMiddlewareService(services, PROJECT_PANEL_ID)
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
return createStore(rootReducer, enhancer);
}
+
+const createRootReducer = (services: ServiceRepository) => combineReducers({
+ auth: authReducer(services),
+ collectionPanel: collectionPanelReducer,
+ collectionPanelFiles: collectionPanelFilesReducer,
+ contextMenu: contextMenuReducer,
+ dataExplorer: dataExplorerReducer,
+ detailsPanel: detailsPanelReducer,
+ dialog: dialogReducer,
+ favorites: favoritesReducer,
+ form: formReducer,
+ properties: propertiesReducer,
+ resources: resourcesReducer,
+ router: routerReducer,
+ snackbar: snackbarReducer,
+ treePicker: treePickerReducer,
+ fileUploader: fileUploaderReducer,
+});
//
// SPDX-License-Identifier: AGPL-3.0
-import { default as unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
+
import { TreePickerNode } from "./tree-picker";
export const treePickerActions = unionize({
- LOAD_TREE_PICKER_NODE: ofType<{ id: string }>(),
- LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ id: string, nodes: Array<TreePickerNode> }>(),
- TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string }>(),
- TOGGLE_TREE_PICKER_NODE_SELECT: ofType<{ id: string }>()
-}, {
- tag: 'type',
- value: 'payload'
- });
+ LOAD_TREE_PICKER_NODE: ofType<{ nodeId: string, pickerId: string }>(),
+ LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ nodeId: string, nodes: Array<TreePickerNode>, pickerId: string }>(),
+ TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ nodeId: string, pickerId: string }>(),
+ TOGGLE_TREE_PICKER_NODE_SELECT: ofType<{ nodeId: string, pickerId: string }>(),
+ EXPAND_TREE_PICKER_NODES: ofType<{ nodeIds: string[], pickerId: string }>(),
+ RESET_TREE_PICKER: ofType<{ pickerId: string }>()
+});
export type TreePickerAction = UnionOf<typeof treePickerActions>;
//
// SPDX-License-Identifier: AGPL-3.0
-import { createTree, getNodeValue, getNodeChildren } from "~/models/tree";
+import { createTree, getNodeValue, getNodeChildrenIds } from "~/models/tree";
import { TreePickerNode, createTreePickerNode } from "./tree-picker";
import { treePickerReducer } from "./tree-picker-reducer";
import { treePickerActions } from "./tree-picker-actions";
describe('TreePickerReducer', () => {
it('LOAD_TREE_PICKER_NODE - initial state', () => {
const tree = createTree<TreePickerNode>();
- const newTree = treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE({ id: '1' }));
- expect(newTree).toEqual(tree);
+ const newState = treePickerReducer({}, treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId: '1', pickerId: "projects" }));
+ expect(newState).toEqual({ 'projects': tree });
});
it('LOAD_TREE_PICKER_NODE', () => {
- const tree = createTree<TreePickerNode>();
- const node = createTreePickerNode({ id: '1', value: '1' });
- const [newTree] = [tree]
- .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
- .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE({ id: '1' })));
- expect(getNodeValue('1')(newTree)).toEqual({
- ...createTreePickerNode({ id: '1', value: '1' }),
+ const node = createTreePickerNode({ nodeId: '1', value: '1' });
+ const [newState] = [{
+ projects: createTree<TreePickerNode>()
+ }]
+ .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [node], pickerId: "projects" })))
+ .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId: '1', pickerId: "projects" })));
+
+ expect(getNodeValue('1')(newState.projects)).toEqual({
+ ...createTreePickerNode({ nodeId: '1', value: '1' }),
status: TreeItemStatus.PENDING
});
});
it('LOAD_TREE_PICKER_NODE_SUCCESS - initial state', () => {
- const tree = createTree<TreePickerNode>();
- const subNode = createTreePickerNode({ id: '1.1', value: '1.1' });
- const newTree = treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [subNode] }));
- expect(getNodeChildren('')(newTree)).toEqual(['1.1']);
+ const subNode = createTreePickerNode({ nodeId: '1.1', value: '1.1' });
+ const newState = treePickerReducer({}, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [subNode], pickerId: "projects" }));
+ expect(getNodeChildrenIds('')(newState.projects)).toEqual(['1.1']);
});
it('LOAD_TREE_PICKER_NODE_SUCCESS', () => {
- const tree = createTree<TreePickerNode>();
- const node = createTreePickerNode({ id: '1', value: '1' });
- const subNode = createTreePickerNode({ id: '1.1', value: '1.1' });
- const [newTree] = [tree]
- .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
- .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '1', nodes: [subNode] })));
- expect(getNodeChildren('1')(newTree)).toEqual(['1.1']);
- expect(getNodeValue('1')(newTree)).toEqual({
- ...createTreePickerNode({ id: '1', value: '1' }),
+ const node = createTreePickerNode({ nodeId: '1', value: '1' });
+ const subNode = createTreePickerNode({ nodeId: '1.1', value: '1.1' });
+ const [newState] = [{
+ projects: createTree<TreePickerNode>()
+ }]
+ .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [node], pickerId: "projects" })))
+ .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '1', nodes: [subNode], pickerId: "projects" })));
+ expect(getNodeChildrenIds('1')(newState.projects)).toEqual(['1.1']);
+ expect(getNodeValue('1')(newState.projects)).toEqual({
+ ...createTreePickerNode({ nodeId: '1', value: '1' }),
status: TreeItemStatus.LOADED
});
});
it('TOGGLE_TREE_PICKER_NODE_COLLAPSE - collapsed', () => {
- const tree = createTree<TreePickerNode>();
- const node = createTreePickerNode({ id: '1', value: '1' });
- const [newTree] = [tree]
- .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
- .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id: '1' })));
- expect(getNodeValue('1')(newTree)).toEqual({
- ...createTreePickerNode({ id: '1', value: '1' }),
+ const node = createTreePickerNode({ nodeId: '1', value: '1' });
+ const [newState] = [{
+ projects: createTree<TreePickerNode>()
+ }]
+ .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [node], pickerId: "projects" })))
+ .map(state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId: '1', pickerId: "projects" })));
+ expect(getNodeValue('1')(newState.projects)).toEqual({
+ ...createTreePickerNode({ nodeId: '1', value: '1' }),
collapsed: false
});
});
it('TOGGLE_TREE_PICKER_NODE_COLLAPSE - expanded', () => {
- const tree = createTree<TreePickerNode>();
- const node = createTreePickerNode({ id: '1', value: '1' });
- const [newTree] = [tree]
- .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
- .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id: '1' })))
- .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id: '1' })));
- expect(getNodeValue('1')(newTree)).toEqual({
- ...createTreePickerNode({ id: '1', value: '1' }),
+ const node = createTreePickerNode({ nodeId: '1', value: '1' });
+ const [newState] = [{
+ projects: createTree<TreePickerNode>()
+ }]
+ .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [node], pickerId: "projects" })))
+ .map(state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId: '1', pickerId: "projects" })))
+ .map(state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId: '1', pickerId: "projects" })));
+ expect(getNodeValue('1')(newState.projects)).toEqual({
+ ...createTreePickerNode({ nodeId: '1', value: '1' }),
collapsed: true
});
});
it('TOGGLE_TREE_PICKER_NODE_SELECT - selected', () => {
- const tree = createTree<TreePickerNode>();
- const node = createTreePickerNode({ id: '1', value: '1' });
- const [newTree] = [tree]
- .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
- .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ id: '1' })));
- expect(getNodeValue('1')(newTree)).toEqual({
- ...createTreePickerNode({ id: '1', value: '1' }),
+ const node = createTreePickerNode({ nodeId: '1', value: '1' });
+ const [newState] = [{
+ projects: createTree<TreePickerNode>()
+ }]
+ .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [node], pickerId: "projects" })))
+ .map(state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId: '1', pickerId: "projects" })));
+ expect(getNodeValue('1')(newState.projects)).toEqual({
+ ...createTreePickerNode({ nodeId: '1', value: '1' }),
selected: true
});
});
it('TOGGLE_TREE_PICKER_NODE_SELECT - not selected', () => {
- const tree = createTree<TreePickerNode>();
- const node = createTreePickerNode({ id: '1', value: '1' });
- const [newTree] = [tree]
- .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
- .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ id: '1' })))
- .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ id: '1' })));
- expect(getNodeValue('1')(newTree)).toEqual({
- ...createTreePickerNode({ id: '1', value: '1' }),
+ const node = createTreePickerNode({ nodeId: '1', value: '1' });
+ const [newState] = [{
+ projects: createTree<TreePickerNode>()
+ }]
+ .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [node], pickerId: "projects" })))
+ .map(state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId: '1', pickerId: "projects" })))
+ .map(state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId: '1', pickerId: "projects" })));
+ expect(getNodeValue('1')(newState.projects)).toEqual({
+ ...createTreePickerNode({ nodeId: '1', value: '1' }),
selected: false
});
});
//
// SPDX-License-Identifier: AGPL-3.0
-import { createTree, setNodeValueWith, TreeNode, setNode, mapTreeValues } from "~/models/tree";
+import { createTree, setNodeValueWith, TreeNode, setNode, mapTreeValues, Tree } from "~/models/tree";
import { TreePicker, TreePickerNode } from "./tree-picker";
import { treePickerActions, TreePickerAction } from "./tree-picker-actions";
import { TreeItemStatus } from "~/components/tree/tree";
+import { compose } from "redux";
+import { getNode } from '../../models/tree';
-export const treePickerReducer = (state: TreePicker = createTree(), action: TreePickerAction) =>
+export const treePickerReducer = (state: TreePicker = {}, action: TreePickerAction) =>
treePickerActions.match(action, {
- LOAD_TREE_PICKER_NODE: ({ id }) =>
- setNodeValueWith(setPending)(id)(state),
- LOAD_TREE_PICKER_NODE_SUCCESS: ({ id, nodes }) => {
- const [newState] = [state]
- .map(receiveNodes(nodes)(id))
- .map(setNodeValueWith(setLoaded)(id));
- return newState;
- },
- TOGGLE_TREE_PICKER_NODE_COLLAPSE: ({ id }) =>
- setNodeValueWith(toggleCollapse)(id)(state),
- TOGGLE_TREE_PICKER_NODE_SELECT: ({ id }) =>
- mapTreeValues(toggleSelect(id))(state),
+ LOAD_TREE_PICKER_NODE: ({ nodeId, pickerId }) =>
+ updateOrCreatePicker(state, pickerId, setNodeValueWith(setPending)(nodeId)),
+ LOAD_TREE_PICKER_NODE_SUCCESS: ({ nodeId, nodes, pickerId }) =>
+ updateOrCreatePicker(state, pickerId, compose(receiveNodes(nodes)(nodeId), setNodeValueWith(setLoaded)(nodeId))),
+ TOGGLE_TREE_PICKER_NODE_COLLAPSE: ({ nodeId, pickerId }) =>
+ updateOrCreatePicker(state, pickerId, setNodeValueWith(toggleCollapse)(nodeId)),
+ TOGGLE_TREE_PICKER_NODE_SELECT: ({ nodeId, pickerId }) =>
+ updateOrCreatePicker(state, pickerId, mapTreeValues(toggleSelect(nodeId))),
+ RESET_TREE_PICKER: ({ pickerId }) =>
+ updateOrCreatePicker(state, pickerId, createTree),
+ EXPAND_TREE_PICKER_NODES: ({ pickerId, nodeIds }) =>
+ updateOrCreatePicker(state, pickerId, mapTreeValues(expand(nodeIds))),
default: () => state
});
+const updateOrCreatePicker = (state: TreePicker, pickerId: string, func: (value: Tree<TreePickerNode>) => Tree<TreePickerNode>) => {
+ const picker = state[pickerId] || createTree();
+ const updatedPicker = func(picker);
+ return { ...state, [pickerId]: updatedPicker };
+};
+
+const expand = (ids: string[]) => (node: TreePickerNode): TreePickerNode =>
+ ids.some(id => id === node.nodeId)
+ ? { ...node, collapsed: false }
+ : node;
+
const setPending = (value: TreePickerNode): TreePickerNode =>
({ ...value, status: TreeItemStatus.PENDING });
const toggleCollapse = (value: TreePickerNode): TreePickerNode =>
({ ...value, collapsed: !value.collapsed });
-const toggleSelect = (id: string) => (value: TreePickerNode): TreePickerNode =>
- value.id === id
+const toggleSelect = (nodeId: string) => (value: TreePickerNode): TreePickerNode =>
+ value.nodeId === nodeId
? ({ ...value, selected: !value.selected })
: ({ ...value, selected: false });
-const receiveNodes = (nodes: Array<TreePickerNode>) => (parent: string) => (state: TreePicker) =>
- nodes.reduce((tree, node) =>
- setNode(
- createTreeNode(parent)(node)
- )(tree), state);
+const receiveNodes = (nodes: Array<TreePickerNode>) => (parent: string) => (state: Tree<TreePickerNode>) => {
+ const parentNode = getNode(parent)(state);
+ let newState = state;
+ if (parentNode) {
+ newState = setNode({ ...parentNode, children: [] })(state);
+ }
+ return nodes.reduce((tree, node) => {
+ const oldNode = getNode(node.nodeId)(state) || { value: {} };
+ const newNode = createTreeNode(parent)(node);
+ const value = { ...oldNode.value, ...newNode.value };
+ return setNode({ ...newNode, value })(tree);
+ }, newState);
+};
const createTreeNode = (parent: string) => (node: TreePickerNode): TreeNode<TreePickerNode> => ({
children: [],
- id: node.id,
+ id: node.nodeId,
parent,
value: node
});
import { Tree } from "~/models/tree";
import { TreeItemStatus } from "~/components/tree/tree";
-export type TreePicker = Tree<TreePickerNode>;
+export type TreePicker = { [key: string]: Tree<TreePickerNode> };
-export interface TreePickerNode {
- id: string;
- value: any;
+export interface TreePickerNode<Value = any> {
+ nodeId: string;
+ value: Value;
selected: boolean;
collapsed: boolean;
status: TreeItemStatus;
}
-export const createTreePickerNode = (data: {id: string, value: any}) => ({
+export const createTreePickerNode = (data: { nodeId: string, value: any }) => ({
...data,
selected: false,
collapsed: true,
status: TreeItemStatus.INITIAL
});
+
+export const getTreePicker = <Value = {}>(id: string) => (state: TreePicker): Tree<TreePickerNode<Value>> | undefined => state[id];
\ 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 { RootState } from "../store";
+import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
+import { loadCollectionPanel } from '~/store/collection-panel/collection-panel-action';
+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 { 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 } 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 { getResource } from '../resources/resources';
+import { getProjectPanelCurrentUuid } from '../project-panel/project-panel-action';
+import * as projectCreateActions from '~/store/projects/project-create-actions';
+import * as projectMoveActions from '~/store/projects/project-move-actions';
+import * as projectUpdateActions from '~/store/projects/project-update-actions';
+import * as collectionCreateActions from '~/store/collections/collection-create-actions';
+import * as collectionCopyActions from '~/store/collections/collection-copy-actions';
+import * as collectionUpdateActions from '~/store/collections/collection-update-actions';
+import * as collectionMoveActions from '~/store/collections/collection-move-actions';
+import * as processesActions from '../processes/processes-actions';
+import { getProcess } from '../processes/process';
+
+
+export const loadWorkbench = () =>
+ async (dispatch: Dispatch, getState: () => RootState) => {
+ const { auth, router } = getState();
+ const { user } = auth;
+ if (user) {
+ const userResource = await dispatch<any>(loadResource(user.uuid));
+ if (userResource) {
+ dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
+ dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
+ dispatch<any>(initSidePanelTree());
+ if (router.location) {
+ const match = matchRootRoute(router.location.pathname);
+ if (match) {
+ dispatch(navigateToProject(userResource.uuid));
+ }
+ }
+ } else {
+ dispatch(userIsNotAuthenticated);
+ }
+ } else {
+ dispatch(userIsNotAuthenticated);
+ }
+ };
+
+export const loadFavorites = () =>
+ (dispatch: Dispatch) => {
+ dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.FAVORITES));
+ dispatch<any>(loadFavoritePanel());
+ dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
+ };
+
+
+export const loadProject = (uuid: string) =>
+ async (dispatch: Dispatch) => {
+ await dispatch<any>(activateSidePanelTreeItem(uuid));
+ dispatch<any>(setProjectBreadcrumbs(uuid));
+ dispatch<any>(openProjectPanel(uuid));
+ dispatch(loadDetailsPanel(uuid));
+ };
+
+export const createProject = (data: projectCreateActions.ProjectCreateFormDialogData) =>
+ async (dispatch: Dispatch) => {
+ const newProject = await dispatch<any>(projectCreateActions.createProject(data));
+ if (newProject) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Project has been successfully created.",
+ hideDuration: 2000
+ }));
+ await dispatch<any>(loadSidePanelTreeProjects(newProject.ownerUuid));
+ dispatch<any>(reloadProjectMatchingUuid([newProject.ownerUuid]));
+ }
+ };
+
+export const moveProject = (data: MoveToFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const oldProject = getResource(data.uuid)(getState().resources);
+ const oldOwnerUuid = oldProject ? oldProject.ownerUuid : '';
+ const movedProject = await dispatch<any>(projectMoveActions.moveProject(data));
+ if (movedProject) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Project has been moved', hideDuration: 2000 }));
+ if (oldProject) {
+ await dispatch<any>(loadSidePanelTreeProjects(oldProject.ownerUuid));
+ }
+ dispatch<any>(reloadProjectMatchingUuid([oldOwnerUuid, movedProject.ownerUuid, movedProject.uuid]));
+ }
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
+ }
+ };
+
+export const updateProject = (data: projectUpdateActions.ProjectUpdateFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const updatedProject = await dispatch<any>(projectUpdateActions.updateProject(data));
+ if (updatedProject) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Project has been successfully updated.",
+ hideDuration: 2000
+ }));
+ await dispatch<any>(loadSidePanelTreeProjects(updatedProject.ownerUuid));
+ dispatch<any>(reloadProjectMatchingUuid([updatedProject.ownerUuid, updatedProject.uuid]));
+ }
+ };
+
+export const loadCollection = (uuid: string) =>
+ async (dispatch: Dispatch) => {
+ const collection = await dispatch<any>(loadCollectionPanel(uuid));
+ await dispatch<any>(activateSidePanelTreeItem(collection.ownerUuid));
+ dispatch<any>(setCollectionBreadcrumbs(collection.uuid));
+ dispatch(loadDetailsPanel(uuid));
+ };
+
+export const createCollection = (data: collectionCreateActions.CollectionCreateFormDialogData) =>
+ async (dispatch: Dispatch) => {
+ const collection = await dispatch<any>(collectionCreateActions.createCollection(data));
+ if (collection) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Collection has been successfully created.",
+ hideDuration: 2000
+ }));
+ dispatch<any>(updateResources([collection]));
+ dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
+ }
+ };
+
+export const updateCollection = (data: collectionUpdateActions.CollectionUpdateFormDialogData) =>
+ async (dispatch: Dispatch) => {
+ const collection = await dispatch<any>(collectionUpdateActions.updateCollection(data));
+ if (collection) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Collection has been successfully updated.",
+ hideDuration: 2000
+ }));
+ dispatch<any>(updateResources([collection]));
+ dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
+ }
+ };
+
+export const copyCollection = (data: collectionCopyActions.CollectionCopyFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const collection = await dispatch<any>(collectionCopyActions.copyCollection(data));
+ dispatch<any>(updateResources([collection]));
+ dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection has been copied.', hideDuration: 2000 }));
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
+ }
+ };
+
+export const moveCollection = (data: MoveToFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const collection = await dispatch<any>(collectionMoveActions.moveCollection(data));
+ dispatch<any>(updateResources([collection]));
+ dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection has been moved.', hideDuration: 2000 }));
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
+ }
+ };
+
+export const loadProcess = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState) => {
+ await dispatch<any>(processesActions.loadProcess(uuid));
+ const process = getProcess(uuid)(getState().resources);
+ if (process) {
+ await dispatch<any>(activateSidePanelTreeItem(process.containerRequest.ownerUuid));
+ dispatch<any>(setCollectionBreadcrumbs(process.containerRequest.ownerUuid));
+ dispatch(loadDetailsPanel(uuid));
+ }
+ };
+
+export const resourceIsNotLoaded = (uuid: string) =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: `Resource identified by ${uuid} is not loaded.`
+ });
+
+export const userIsNotAuthenticated = snackbarActions.OPEN_SNACKBAR({
+ message: 'User is not authenticated'
+});
+
+export const couldNotLoadUser = snackbarActions.OPEN_SNACKBAR({
+ message: 'Could not load user'
+});
+
+const reloadProjectMatchingUuid = (matchingUuids: string[]) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const currentProjectPanelUuid = getProjectPanelCurrentUuid(getState());
+ if (currentProjectPanelUuid && matchingUuids.some(uuid => uuid === currentProjectPanelUuid)) {
+ dispatch<any>(loadProject(currentProjectPanelUuid));
+ }
+ };
\ No newline at end of file
export const COLLECTION_NAME_VALIDATION = [require, maxLength(255)];
export const COLLECTION_DESCRIPTION_VALIDATION = [maxLength(255)];
-export const COLLECTION_PROJECT_VALIDATION = [require];
\ No newline at end of file
+export const COLLECTION_PROJECT_VALIDATION = [require];
+
+export const COPY_NAME_VALIDATION = [require, maxLength(255)];
+export const COPY_FILE_VALIDATION = [require];
+
+export const MOVE_TO_VALIDATION = [require];
import * as React from "react";
import { connect, DispatchProp } from "react-redux";
import { getUserDetails, saveApiToken } from "~/store/auth/auth-action";
-import { getProjectList } from "~/store/project/project-action";
import { getUrlParameter } from "~/common/url";
import { AuthService } from "~/services/auth-service/auth-service";
const search = this.props.location ? this.props.location.search : "";
const apiToken = getUrlParameter(search, 'api_token');
this.props.dispatch(saveApiToken(apiToken));
- this.props.dispatch<any>(getUserDetails()).then(() => {
- const rootUuid = this.props.authService.getRootUuid();
- this.props.dispatch(getProjectList(rootUuid));
- });
+ this.props.dispatch<any>(getUserDetails());
}
render() {
return <Redirect to="/"/>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { Breadcrumbs as BreadcrumbsComponent, BreadcrumbsProps } from '~/components/breadcrumbs/breadcrumbs';
+import { RootState } from '~/store/store';
+import { Dispatch } from 'redux';
+import { navigateTo } from '~/store/navigation/navigation-action';
+import { getProperty } from '../../store/properties/properties';
+import { ResourceBreadcrumb, BREADCRUMBS } from '../../store/breadcrumbs/breadcrumbs-actions';
+import { openSidePanelContextMenu } from '~/store/context-menu/context-menu-actions';
+
+type BreadcrumbsDataProps = Pick<BreadcrumbsProps, 'items'>;
+type BreadcrumbsActionProps = Pick<BreadcrumbsProps, 'onClick' | 'onContextMenu'>;
+
+const mapStateToProps = () => ({ properties }: RootState): BreadcrumbsDataProps => ({
+ items: getProperty<ResourceBreadcrumb[]>(BREADCRUMBS)(properties) || []
+});
+
+const mapDispatchToProps = (dispatch: Dispatch): BreadcrumbsActionProps => ({
+ onClick: ({ uuid }: ResourceBreadcrumb) => {
+ dispatch<any>(navigateTo(uuid));
+ },
+ onContextMenu: (event, breadcrumb: ResourceBreadcrumb) => {
+ dispatch<any>(openSidePanelContextMenu(event, breadcrumb.uuid));
+ }
+});
+
+export const Breadcrumbs = connect(mapStateToProps(), mapDispatchToProps)(BreadcrumbsComponent);
\ No newline at end of file
import { FileTreeData } from "~/components/file-tree/file-tree-data";
import { Dispatch } from "redux";
import { collectionPanelFilesAction } from "~/store/collection-panel/collection-panel-files/collection-panel-files-actions";
-import { contextMenuActions } from "~/store/context-menu/context-menu-actions";
import { ContextMenuKind } from "../context-menu/context-menu";
-import { Tree, getNodeChildren, getNode } from "~/models/tree";
+import { Tree, getNodeChildrenIds, getNode } from "~/models/tree";
import { CollectionFileType } from "~/models/collection-file";
+import { openContextMenu } from '~/store/context-menu/context-menu-actions';
+import { openUploadCollectionFilesDialog } from '~/store/collections/collection-upload-actions';
const memoizedMapStateToProps = () => {
let prevState: CollectionPanelFilesState;
return (state: RootState): Pick<CollectionPanelFilesProps, "items"> => {
if (prevState !== state.collectionPanelFiles) {
prevState = state.collectionPanelFiles;
- prevTree = getNodeChildren('')(state.collectionPanelFiles)
+ prevTree = getNodeChildrenIds('')(state.collectionPanelFiles)
.map(collectionItemToTreeItem(state.collectionPanelFiles));
}
return {
};
const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps, 'onUploadDataClick' | 'onCollapseToggle' | 'onSelectionToggle' | 'onItemMenuOpen' | 'onOptionsMenuOpen'> => ({
- onUploadDataClick: () => { return; },
+ onUploadDataClick: () => {
+ dispatch<any>(openUploadCollectionFilesDialog());
+ },
onCollapseToggle: (id) => {
dispatch(collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_COLLAPSE({ id }));
},
dispatch(collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: item.id }));
},
onItemMenuOpen: (event, item) => {
- event.preventDefault();
- dispatch(contextMenuActions.OPEN_CONTEXT_MENU({
- position: { x: event.clientX, y: event.clientY },
- resource: { kind: ContextMenuKind.COLLECTION_FILES_ITEM, name: item.data.name, uuid: item.id }
- }));
+ dispatch<any>(openContextMenu(event, { kind: ContextMenuKind.COLLECTION_FILES_ITEM, name: item.data.name, uuid: item.id }));
+ },
+ onOptionsMenuOpen: (event) => {
+ dispatch<any>(openContextMenu(event, { kind: ContextMenuKind.COLLECTION_FILES, name: '', uuid: '' }));
},
- onOptionsMenuOpen: (event) =>
- dispatch(contextMenuActions.OPEN_CONTEXT_MENU({
- position: { x: event.clientX, y: event.clientY },
- resource: { kind: ContextMenuKind.COLLECTION_FILES, name: '', uuid: '' }
- }))
});
type: node.value.type
},
id: node.id,
- items: getNodeChildren(node.id)(tree)
+ items: getNodeChildrenIds(node.id)(tree)
.map(collectionItemToTreeItem(tree)),
open: node.value.type === CollectionFileType.DIRECTORY ? !node.value.collapsed : false,
selected: node.value.selected,
import { ToggleFavoriteAction } from "../actions/favorite-action";
import { toggleFavorite } from "~/store/favorites/favorites-actions";
import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, ProvenanceGraphIcon, AdvancedIcon, RemoveIcon } from "~/components/icon/icon";
-import { openUpdater } from "~/store/collections/updater/collection-updater-action";
+import { openCollectionUpdateDialog } from "~/store/collections/collection-update-actions";
import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
+import { openMoveCollectionDialog } from '~/store/collections/collection-move-actions';
+import { openCollectionCopyDialog } from "~/store/collections/collection-copy-actions";
import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
import { toggleCollectionTrashed } from "~/store/collections/collection-trash-actions";
icon: RenameIcon,
name: "Edit collection",
execute: (dispatch, resource) => {
- dispatch<any>(openUpdater(resource));
+ dispatch<any>(openCollectionUpdateDialog(resource));
}
},
{
{
icon: MoveToIcon,
name: "Move to",
- execute: (dispatch, resource) => {
- // add code
- }
+ execute: (dispatch, resource) => dispatch<any>(openMoveCollectionDialog(resource))
},
{
component: ToggleFavoriteAction,
icon: CopyIcon,
name: "Copy to project",
execute: (dispatch, resource) => {
- // add code
+ dispatch<any>(openCollectionCopyDialog(resource));
}
},
{
import { ContextMenuActionSet } from "../context-menu-action-set";
import { collectionPanelFilesAction, openMultipleFilesRemoveDialog } from "~/store/collection-panel/collection-panel-files/collection-panel-files-actions";
-import { createCollectionWithSelected } from "~/views-components/create-collection-dialog-with-selected/create-collection-dialog-with-selected";
-
+import { openCollectionPartialCopyDialog } from '~/store/collections/collection-partial-copy-actions';
export const collectionFilesActionSet: ContextMenuActionSet = [[{
name: "Select all",
}, {
name: "Create a new collection with selected",
execute: (dispatch) => {
- dispatch<any>(createCollectionWithSelected());
+ dispatch<any>(openCollectionPartialCopyDialog());
}
}]];
// SPDX-License-Identifier: AGPL-3.0
import { ContextMenuActionSet } from "../context-menu-action-set";
-import { RenameIcon, DownloadIcon, RemoveIcon } from "~/components/icon/icon";
-import { openRenameFileDialog } from "../../rename-file-dialog/rename-file-dialog";
+import { RenameIcon, RemoveIcon } from "~/components/icon/icon";
import { DownloadCollectionFileAction } from "../actions/download-collection-file-action";
-import { openFileRemoveDialog } from "../../../store/collection-panel/collection-panel-files/collection-panel-files-actions";
+import { openFileRemoveDialog, openRenameFileDialog } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions';
export const collectionFilesItemActionSet: ContextMenuActionSet = [[{
name: "Rename",
icon: RenameIcon,
execute: (dispatch, resource) => {
- dispatch<any>(openRenameFileDialog(resource.name));
+ dispatch<any>(openRenameFileDialog({ name: resource.name, id: resource.uuid }));
}
}, {
component: DownloadCollectionFileAction,
import { ContextMenuActionSet } from "../context-menu-action-set";
import { ToggleFavoriteAction } from "../actions/favorite-action";
+import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
import { toggleFavorite } from "~/store/favorites/favorites-actions";
import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, RemoveIcon } from "~/components/icon/icon";
-import { openUpdater } from "~/store/collections/updater/collection-updater-action";
+import { openCollectionUpdateDialog } from "~/store/collections/collection-update-actions";
import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
-import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
-import { toggleCollectionTrashed } from "~/store/collections/collection-trash-actions";
+import { openMoveCollectionDialog } from '~/store/collections/collection-move-actions';
+import { openCollectionCopyDialog } from '~/store/collections/collection-copy-actions';
export const collectionResourceActionSet: ContextMenuActionSet = [[
{
icon: RenameIcon,
name: "Edit collection",
execute: (dispatch, resource) => {
- dispatch<any>(openUpdater(resource));
+ dispatch<any>(openCollectionUpdateDialog(resource));
}
},
{
{
icon: MoveToIcon,
name: "Move to",
- execute: (dispatch, resource) => {
- // add code
- }
+ execute: (dispatch, resource) => dispatch<any>(openMoveCollectionDialog(resource))
},
{
component: ToggleFavoriteAction,
icon: CopyIcon,
name: "Copy to project",
execute: (dispatch, resource) => {
- // add code
- }
+ dispatch<any>(openCollectionCopyDialog(resource));
+ },
},
{
icon: DetailsIcon,
--- /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, ProvenanceGraphIcon,
+ AdvancedIcon, RemoveIcon, ReRunProcessIcon, LogIcon
+} from "~/components/icon/icon";
+import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
+
+export const processActionSet: ContextMenuActionSet = [[
+ {
+ icon: RenameIcon,
+ name: "Edit process",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ icon: ShareIcon,
+ name: "Share",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ icon: MoveToIcon,
+ name: "Move to",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ component: ToggleFavoriteAction,
+ execute: (dispatch, resource) => {
+ dispatch<any>(toggleFavorite(resource)).then(() => {
+ dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
+ });
+ }
+ },
+ {
+ icon: CopyIcon,
+ name: "Copy to project",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ icon: ReRunProcessIcon,
+ name: "Re-run process",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ name: "Inputs",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ name: "Outputs",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ name: "Command",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ icon: DetailsIcon,
+ name: "View details",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ icon: LogIcon,
+ name: "Log",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ icon: ProvenanceGraphIcon,
+ name: "Provenance graph",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ icon: AdvancedIcon,
+ name: "Advanced",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ },
+ {
+ icon: RemoveIcon,
+ name: "Remove",
+ execute: (dispatch, resource) => {
+ // add code
+ }
+ }
+]];
//
// SPDX-License-Identifier: AGPL-3.0
-import { reset } from "redux-form";
-
import { ContextMenuActionSet } from "../context-menu-action-set";
-import { projectActions } from "~/store/project/project-action";
-import { collectionCreateActions } from "~/store/collections/creator/collection-creator-action";
-import { PROJECT_CREATE_DIALOG } from "../../dialog-create/dialog-project-create";
-import { COLLECTION_CREATE_DIALOG } from "../../dialog-create/dialog-collection-create";
+import { openCollectionCreateDialog } from '~/store/collections/collection-create-actions';
import { NewProjectIcon, CollectionIcon } from "~/components/icon/icon";
+import { openProjectCreateDialog } from '~/store/projects/project-create-actions';
export const rootProjectActionSet: ContextMenuActionSet = [[
{
icon: NewProjectIcon,
name: "New project",
execute: (dispatch, resource) => {
- dispatch(reset(PROJECT_CREATE_DIALOG));
- dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid }));
+ dispatch<any>(openProjectCreateDialog(resource.uuid));
}
},
{
icon: CollectionIcon,
name: "New Collection",
execute: (dispatch, resource) => {
- dispatch(reset(COLLECTION_CREATE_DIALOG));
- dispatch(collectionCreateActions.OPEN_COLLECTION_CREATOR({ ownerUuid: resource.uuid }));
+ dispatch<any>(openCollectionCreateDialog(resource.uuid));
}
}
]];
COLLECTION_FILES = "CollectionFiles",
COLLECTION_FILES_ITEM = "CollectionFilesItem",
COLLECTION = 'Collection',
- COLLECTION_RESOURCE = 'CollectionResource'
+ COLLECTION_RESOURCE = 'CollectionResource',
+ PROCESS = "Process"
}
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { Dispatch } from "redux";
-import { reduxForm, reset, startSubmit, stopSubmit } from "redux-form";
-import { withDialog } from "~/store/dialog/with-dialog";
-import { dialogActions } from "~/store/dialog/dialog-actions";
-import { DialogCollectionCreateWithSelected } from "../dialog-create/dialog-collection-create-selected";
-import { loadProjectTreePickerProjects } from "../project-tree-picker/project-tree-picker";
-
-export const DIALOG_COLLECTION_CREATE_WITH_SELECTED = 'dialogCollectionCreateWithSelected';
-
-export const createCollectionWithSelected = () =>
- (dispatch: Dispatch) => {
- dispatch(reset(DIALOG_COLLECTION_CREATE_WITH_SELECTED));
- dispatch<any>(loadProjectTreePickerProjects(''));
- dispatch(dialogActions.OPEN_DIALOG({ id: DIALOG_COLLECTION_CREATE_WITH_SELECTED, data: {} }));
- };
-
-export const [DialogCollectionCreateWithSelectedFile] = [DialogCollectionCreateWithSelected]
- .map(withDialog(DIALOG_COLLECTION_CREATE_WITH_SELECTED))
- .map(reduxForm({
- form: DIALOG_COLLECTION_CREATE_WITH_SELECTED,
- onSubmit: (data, dispatch) => {
- dispatch(startSubmit(DIALOG_COLLECTION_CREATE_WITH_SELECTED));
- setTimeout(() => dispatch(stopSubmit(DIALOG_COLLECTION_CREATE_WITH_SELECTED, { name: 'Invalid name' })), 2000);
- }
- }));
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { connect } from "react-redux";
-import { Dispatch } from "redux";
-import { SubmissionError } from "redux-form";
-
-import { RootState } from "~/store/store";
-import { DialogCollectionCreate } from "../dialog-create/dialog-collection-create";
-import { collectionCreateActions, createCollection } from "~/store/collections/creator/collection-creator-action";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
-import { UploadFile } from "~/store/collections/uploader/collection-uploader-actions";
-import { projectPanelActions } from "~/store/project-panel/project-panel-action";
-
-const mapStateToProps = (state: RootState) => ({
- open: state.collections.creator.opened
-});
-
-const mapDispatchToProps = (dispatch: Dispatch) => ({
- handleClose: () => {
- dispatch(collectionCreateActions.CLOSE_COLLECTION_CREATOR());
- },
- onSubmit: (data: { name: string, description: string }, files: UploadFile[]) => {
- return dispatch<any>(addCollection(data, files.map(f => f.file)))
- .catch((e: any) => {
- throw new SubmissionError({ name: e.errors.join("").includes("UniqueViolation") ? "Collection with this name already exists." : "" });
- });
- }
-});
-
-const addCollection = (data: { name: string, description: string }, files: File[]) =>
- (dispatch: Dispatch) => {
- return dispatch<any>(createCollection(data, files)).then(() => {
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Collection has been successfully created.",
- hideDuration: 2000
- }));
- dispatch(projectPanelActions.REQUEST_ITEMS());
- });
- };
-
-export const CreateCollectionDialog = connect(mapStateToProps, mapDispatchToProps)(DialogCollectionCreate);
-
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { connect } from "react-redux";
-import { Dispatch } from "redux";
-import { SubmissionError } from "redux-form";
-
-import { RootState } from "~/store/store";
-import { DialogProjectCreate } from "../dialog-create/dialog-project-create";
-import { projectActions, createProject, getProjectList } from "~/store/project/project-action";
-import { projectPanelActions } from "~/store/project-panel/project-panel-action";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
-
-const mapStateToProps = (state: RootState) => ({
- open: state.projects.creator.opened
-});
-
-const addProject = (data: { name: string, description: string }) =>
- (dispatch: Dispatch, getState: () => RootState) => {
- const { ownerUuid } = getState().projects.creator;
- return dispatch<any>(createProject(data)).then(() => {
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Project has been successfully created.",
- hideDuration: 2000
- }));
- dispatch(projectPanelActions.REQUEST_ITEMS());
- dispatch<any>(getProjectList(ownerUuid));
- });
- };
-
-const mapDispatchToProps = (dispatch: Dispatch) => ({
- handleClose: () => {
- dispatch(projectActions.CLOSE_PROJECT_CREATOR());
- },
- onSubmit: (data: { name: string, description: string }) => {
- return dispatch<any>(addProject(data))
- .catch((e: any) => {
- throw new SubmissionError({ name: e.errors.join("").includes("UniqueViolation") ? "Project with this name already exists." : "" });
- });
- }
-});
-
-export const CreateProjectDialog = connect(mapStateToProps, mapDispatchToProps)(DialogProjectCreate);
interface Props {
id: string;
- columns: DataColumns<any>;
onRowClick: (item: any) => void;
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => void;
onRowDoubleClick: (item: any) => void;
extractKey?: (item: any) => React.Key;
}
-const mapStateToProps = (state: RootState, { id, columns }: Props) => {
- const s = getDataExplorer(state.dataExplorer, id);
- if (s.columns.length === 0) {
- s.columns = columns;
- }
- return s;
+const mapStateToProps = (state: RootState, { id }: Props) => {
+ return getDataExplorer(state.dataExplorer, id);
};
const mapDispatchToProps = () => {
- return (dispatch: Dispatch, { id, columns, onRowClick, onRowDoubleClick, onContextMenu }: Props) => ({
+ return (dispatch: Dispatch, { id, onRowClick, onRowDoubleClick, onContextMenu }: Props) => ({
onSetColumns: (columns: DataColumns<any>) => {
dispatch(dataExplorerActions.SET_COLUMNS({ id, columns }));
},
import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon } from '~/components/icon/icon';
import { formatDate, formatFileSize } from '~/common/formatters';
import { resourceLabel } from '~/common/labels';
+import { connect } from 'react-redux';
+import { RootState } from '~/store/store';
+import { getResource } from '../../store/resources/resources';
+import { GroupContentsResource } from '~/services/groups-service/groups-service';
+import { ProcessResource } from '~/models/process';
-export const renderName = (item: {name: string; uuid: string, kind: string}) =>
+export const renderName = (item: { name: string; uuid: string, kind: string }) =>
<Grid container alignItems="center" wrap="nowrap" spacing={16}>
<Grid item>
{renderIcon(item)}
</Grid>
</Grid>;
+export const ResourceName = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ return resource || { name: '', uuid: '', kind: '' };
+ })(renderName);
-export const renderIcon = (item: {kind: string}) => {
+export const renderIcon = (item: { kind: string }) => {
switch (item.kind) {
case ResourceKind.PROJECT:
return <ProjectIcon />;
return <Typography noWrap>{formatDate(date)}</Typography>;
};
+export const ResourceLastModifiedDate = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ return { date: resource ? resource.modifiedAt : '' };
+ })((props: { date: string }) => renderDate(props.date));
+
export const renderFileSize = (fileSize?: number) =>
<Typography noWrap>
{formatFileSize(fileSize)}
</Typography>;
+export const ResourceFileSize = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ return {};
+ })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
+
export const renderOwner = (owner: string) =>
<Typography noWrap color="primary" >
{owner}
</Typography>;
+export const ResourceOwner = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ return { owner: resource ? resource.ownerUuid : '' };
+ })((props: { owner: string }) => renderOwner(props.owner));
+
export const renderType = (type: string) =>
<Typography noWrap>
{resourceLabel(type)}
</Typography>;
-export const renderStatus = (item: {status?: string}) =>
+export const ResourceType = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ return { type: resource ? resource.kind : '' };
+ })((props: { type: string }) => renderType(props.type));
+
+export const renderStatus = (item: { status?: string }) =>
<Typography noWrap align="center" >
{item.status || "-"}
</Typography>;
+
+export const ProcessStatus = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource(props.uuid)(state.resources) as ProcessResource | undefined;
+ return { status: resource ? resource.state : '-' };
+ })((props: { status: string }) => renderType(props.status));
return <div>
<DetailsAttribute label='Type' value={resourceLabel(ResourceKind.COLLECTION)} />
<DetailsAttribute label='Size' value='---' />
- <DetailsAttribute label='Owner' value={this.item.ownerUuid} />
+ <DetailsAttribute label='Owner' value={this.item.ownerUuid} lowercaseValue={true} />
<DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
<DetailsAttribute label='Created at' value={formatDate(this.item.createdAt)} />
{/* Links but we dont have view */}
import { EmptyDetails } from "./empty-details";
import { DetailsData } from "./details-data";
import { DetailsResource } from "~/models/details";
+import { getResource } from '../../store/resources/resources';
-type CssRules = 'drawerPaper' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'tabContainer';
+type CssRules = 'drawerPaper' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'headerTitle' | 'tabContainer';
const drawerWidth = 320;
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
textAlign: 'center'
},
headerIcon: {
- fontSize: "34px"
+ fontSize: '2.125rem'
+ },
+ headerTitle: {
+ overflowWrap: 'break-word',
+ wordWrap: 'break-word'
},
tabContainer: {
padding: theme.spacing.unit * 3
}
};
-const mapStateToProps = ({ detailsPanel }: RootState) => ({
- isOpened: detailsPanel.isOpened,
- item: getItem(detailsPanel.item as DetailsResource)
-});
+const mapStateToProps = ({ detailsPanel, resources }: RootState) => {
+ const resource = getResource(detailsPanel.resourceUuid)(resources) as DetailsResource;
+ return {
+ isOpened: detailsPanel.isOpened,
+ item: getItem(resource)
+ };
+};
const mapDispatchToProps = (dispatch: Dispatch) => ({
onCloseDrawer: () => {
const { tabsValue } = this.state;
return (
<Typography component="div"
- className={classnames([classes.container, { [classes.opened]: isOpened }])}>
+ 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'>
{item.getIcon(classes.headerIcon)}
</Grid>
<Grid item xs={8}>
- <Typography variant="title">
+ <Typography variant="title" className={classes.headerTitle}>
{item.getTitle()}
</Typography>
</Grid>
<Grid item>
<IconButton color="inherit" onClick={onCloseDrawer}>
- {<CloseIcon/>}
+ {<CloseIcon />}
</IconButton>
</Grid>
</Grid>
</Typography>
<Tabs value={tabsValue} onChange={this.handleChange}>
- <Tab disableRipple label="Details"/>
- <Tab disableRipple label="Activity" disabled/>
+ <Tab disableRipple label="Details" />
+ <Tab disableRipple label="Activity" disabled />
</Tabs>
{tabsValue === 0 && this.renderTabContainer(
<Grid container direction="column">
</Grid>
)}
{tabsValue === 1 && this.renderTabContainer(
- <Grid container direction="column"/>
+ <Grid container direction="column" />
)}
</Drawer>
</Typography>
return <div>
<DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROCESS)} />
<DetailsAttribute label='Size' value='---' />
- <DetailsAttribute label='Owner' value={this.item.ownerUuid} />
+ <DetailsAttribute label='Owner' value={this.item.ownerUuid} lowercaseValue={true} />
{/* Missing attr */}
<DetailsAttribute label='Status' value={this.item.state} />
{/* Links but we dont have view */}
<DetailsAttribute label='Outputs' link={this.item.outputPath} value={this.item.outputPath} />
<DetailsAttribute label='UUID' link={this.item.uuid} value={this.item.uuid} />
- <DetailsAttribute label='Container UUID' link={this.item.containerUuid} value={this.item.containerUuid} />
+ <DetailsAttribute label='Container UUID' link={this.item.containerUuid || ''} value={this.item.containerUuid} />
<DetailsAttribute label='Priority' value={this.item.priority} />
- <DetailsAttribute label='Runtime Constraints' value={this.item.runtimeConstraints} />
+ <DetailsAttribute label='Runtime Constraints' value={JSON.stringify(this.item.runtimeConstraints)} />
{/* Link but we dont have view */}
<DetailsAttribute label='Docker Image locator' link={this.item.containerImage} value={this.item.containerImage} />
</div>;
<DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROJECT)} />
{/* Missing attr */}
<DetailsAttribute label='Size' value='---' />
- <DetailsAttribute label='Owner' value={this.item.ownerUuid} />
+ <DetailsAttribute label='Owner' value={this.item.ownerUuid} lowercaseValue={true} />
<DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
<DetailsAttribute label='Created at' value={formatDate(this.item.createdAt)} />
{/* Missing attr */}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { InjectedFormProps, Field } from 'redux-form';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+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";
+
+type CopyFormDialogProps = WithDialogProps<string> & InjectedFormProps<CollectionCopyFormDialogData>;
+
+export const DialogCollectionCopy = (props: CopyFormDialogProps) =>
+ <FormDialog
+ dialogTitle='Make a copy'
+ formFields={CollectionCopyFields}
+ submitLabel='Copy'
+ {...props}
+ />;
+
+const CollectionCopyFields = () => <span>
+ <Field
+ name='name'
+ component={TextField}
+ validate={COPY_NAME_VALIDATION}
+ label="Enter a new name for the copy" />
+ <Field
+ name="ownerUuid"
+ component={ProjectTreePickerField}
+ validate={COPY_FILE_VALIDATION} />
+</span>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { CollectionNameField, CollectionDescriptionField, CollectionProjectPickerField } from '~/views-components/form-fields/collection-form-fields';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { InjectedFormProps } from 'redux-form';
+import { CollectionPartialCopyFormData } from '~/store/collections/collection-partial-copy-actions';
+
+type DialogCollectionPartialCopyProps = WithDialogProps<string> & InjectedFormProps<CollectionPartialCopyFormData>;
+
+export const DialogCollectionPartialCopy = (props: DialogCollectionPartialCopyProps) =>
+ <FormDialog
+ dialogTitle='Create a collection'
+ formFields={CollectionPartialCopyFields}
+ submitLabel='Create a collection'
+ {...props}
+ />;
+
+export const CollectionPartialCopyFields = () => <div style={{ display: 'flex' }}>
+ <div>
+ <CollectionNameField />
+ <CollectionDescriptionField />
+ </div>
+ <CollectionProjectPickerField />
+</div>;
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from "react";
-import { InjectedFormProps, Field, WrappedFieldProps } from "redux-form";
-import { Dialog, DialogTitle, DialogContent, DialogActions, Button, CircularProgress } from "@material-ui/core";
-import { WithDialogProps } from "~/store/dialog/with-dialog";
-import { TextField } from "~/components/text-field/text-field";
-import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION, COLLECTION_PROJECT_VALIDATION } from "~/validators/validators";
-import { ProjectTreePicker } from "../project-tree-picker/project-tree-picker";
-
-export const DialogCollectionCreateWithSelected = (props: WithDialogProps<string> & InjectedFormProps<{ name: string }>) =>
- <form>
- <Dialog open={props.open}
- disableBackdropClick={true}
- disableEscapeKeyDown={true}>
- <DialogTitle>Create a collection</DialogTitle>
- <DialogContent style={{ display: 'flex' }}>
- <div>
- <Field
- name='name'
- component={TextField}
- validate={COLLECTION_NAME_VALIDATION}
- label="Collection Name" />
- <Field
- name='description'
- component={TextField}
- validate={COLLECTION_DESCRIPTION_VALIDATION}
- label="Description - optional" />
- </div>
- <Field
- name="projectUuid"
- component={Picker}
- validate={COLLECTION_PROJECT_VALIDATION} />
- </DialogContent>
- <DialogActions>
- <Button
- variant='flat'
- color='primary'
- disabled={props.submitting}
- onClick={props.closeDialog}>
- Cancel
- </Button>
- <Button
- variant='contained'
- color='primary'
- type='submit'
- onClick={props.handleSubmit}
- disabled={props.pristine || props.invalid || props.submitting}>
- {props.submitting
- ? <CircularProgress size={20} />
- : 'Create a collection'}
- </Button>
- </DialogActions>
- </Dialog>
- </form>;
-
-const Picker = (props: WrappedFieldProps) =>
- <div style={{ width: '400px', height: '144px', display: 'flex', flexDirection: 'column' }}>
- <ProjectTreePicker onChange={projectUuid => props.input.onChange(projectUuid)} />
- </div>;
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { reduxForm, Field } from 'redux-form';
-import { compose } from 'redux';
-import { TextField } from '~/components/text-field/text-field';
-import { Dialog, DialogActions, DialogContent, DialogTitle } from '@material-ui/core/';
-import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
+import { InjectedFormProps, Field } from 'redux-form';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { CollectionCreateFormDialogData } from '~/store/collections/collection-create-actions';
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { CollectionNameField, CollectionDescriptionField } from '~/views-components/form-fields/collection-form-fields';
+import { require } from '~/validators/require';
+import { FileUploaderField } from '../file-uploader/file-uploader';
+
+
+type DialogCollectionProps = WithDialogProps<{}> & InjectedFormProps<CollectionCreateFormDialogData>;
+
+export const DialogCollectionCreate = (props: DialogCollectionProps) =>
+ <FormDialog
+ dialogTitle='Create a collection'
+ formFields={CollectionAddFields}
+ submitLabel='Create a Collection'
+ {...props}
+ />;
+
+const CollectionAddFields = () => <span>
+ <CollectionNameField />
+ <CollectionDescriptionField />
+ <Field
+ name='files'
+ validate={[require]}
+ label='Files'
+ component={FileUploaderField} />
+</span>;
-import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '~/validators/validators';
-import { FileUpload } from "~/components/file-upload/file-upload";
-import { connect, DispatchProp } from "react-redux";
-import { RootState } from "~/store/store";
-import { collectionUploaderActions, UploadFile } from "~/store/collections/uploader/collection-uploader-actions";
-
-type CssRules = "button" | "lastButton" | "formContainer" | "createProgress" | "dialogActions";
-
-const styles: StyleRulesCallback<CssRules> = theme => ({
- button: {
- marginLeft: theme.spacing.unit
- },
- lastButton: {
- marginLeft: theme.spacing.unit,
- marginRight: "20px",
- },
- formContainer: {
- display: "flex",
- flexDirection: "column",
- },
- createProgress: {
- position: "absolute",
- minWidth: "20px",
- right: "110px"
- },
- dialogActions: {
- marginBottom: theme.spacing.unit * 3
- }
-});
-
-interface DialogCollectionDataProps {
- open: boolean;
- handleSubmit: any;
- submitting: boolean;
- invalid: boolean;
- pristine: boolean;
- files: UploadFile[];
-}
-
-interface DialogCollectionActionProps {
- handleClose: () => void;
- onSubmit: (data: { name: string, description: string }, files: UploadFile[]) => void;
-}
-
-type DialogCollectionProps = DialogCollectionDataProps & DialogCollectionActionProps & DispatchProp & WithStyles<CssRules>;
-
-export const COLLECTION_CREATE_DIALOG = "collectionCreateDialog";
-
-export const DialogCollectionCreate = compose(
- connect((state: RootState) => ({
- files: state.collections.uploader
- })),
- reduxForm({ form: COLLECTION_CREATE_DIALOG }),
- withStyles(styles))(
- class DialogCollectionCreate extends React.Component<DialogCollectionProps> {
- render() {
- const { classes, open, handleClose, handleSubmit, onSubmit, submitting, invalid, pristine, files } = this.props;
- const busy = submitting || files.reduce(
- (prev, curr) => prev + (curr.loaded > 0 && curr.loaded < curr.total ? 1 : 0), 0
- ) > 0;
- return (
- <Dialog
- open={open}
- onClose={handleClose}
- fullWidth={true}
- maxWidth='sm'
- disableBackdropClick={true}
- disableEscapeKeyDown={true}>
- <form onSubmit={handleSubmit((data: any) => onSubmit(data, files))}>
- <DialogTitle id="form-dialog-title">Create a collection</DialogTitle>
- <DialogContent className={classes.formContainer}>
- <Field name="name"
- disabled={submitting}
- component={TextField}
- validate={COLLECTION_NAME_VALIDATION}
- label="Collection Name" />
- <Field name="description"
- disabled={submitting}
- component={TextField}
- validate={COLLECTION_DESCRIPTION_VALIDATION}
- label="Description - optional" />
- <FileUpload
- files={files}
- disabled={busy}
- onDrop={files => this.props.dispatch(collectionUploaderActions.SET_UPLOAD_FILES(files))} />
- </DialogContent>
- <DialogActions className={classes.dialogActions}>
- <Button onClick={handleClose} className={classes.button} color="primary"
- disabled={busy}>CANCEL</Button>
- <Button type="submit"
- className={classes.lastButton}
- color="primary"
- disabled={invalid || busy || pristine}
- variant="contained">
- CREATE A COLLECTION
- </Button>
- {busy && <CircularProgress size={20} className={classes.createProgress} />}
- </DialogActions>
- </form>
- </Dialog>
- );
- }
- }
- );
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { reduxForm, Field } from 'redux-form';
-import { compose } from 'redux';
-import { TextField } from '~/components/text-field/text-field';
-import { Dialog, DialogActions, DialogContent, DialogTitle } from '@material-ui/core/';
-import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
+import { InjectedFormProps } from 'redux-form';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { ProjectCreateFormDialogData } from '~/store/projects/project-create-actions';
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { ProjectNameField, ProjectDescriptionField } from '~/views-components/form-fields/project-form-fields';
-import { PROJECT_NAME_VALIDATION, PROJECT_DESCRIPTION_VALIDATION } from '~/validators/validators';
+type DialogCollectionProps = WithDialogProps<{}> & InjectedFormProps<ProjectCreateFormDialogData>;
-type CssRules = "button" | "lastButton" | "formContainer" | "dialog" | "dialogTitle" | "createProgress" | "dialogActions";
+export const DialogProjectCreate = (props: DialogCollectionProps) =>
+ <FormDialog
+ dialogTitle='Create a project'
+ formFields={ProjectAddFields}
+ submitLabel='Create a Project'
+ {...props}
+ />;
-const styles: StyleRulesCallback<CssRules> = theme => ({
- button: {
- marginLeft: theme.spacing.unit
- },
- lastButton: {
- marginLeft: theme.spacing.unit,
- marginRight: "20px",
- },
- formContainer: {
- display: "flex",
- flexDirection: "column",
- marginTop: "20px",
- },
- dialogTitle: {
- paddingBottom: "0"
- },
- dialog: {
- minWidth: "600px",
- minHeight: "320px"
- },
- createProgress: {
- position: "absolute",
- minWidth: "20px",
- right: "95px"
- },
- dialogActions: {
- marginBottom: "24px"
- }
-});
-interface DialogProjectProps {
- open: boolean;
- handleClose: () => void;
- onSubmit: (data: { name: string, description: string }) => void;
- handleSubmit: any;
- submitting: boolean;
- invalid: boolean;
- pristine: boolean;
-}
-
-export const PROJECT_CREATE_DIALOG = "projectCreateDialog";
-
-export const DialogProjectCreate = compose(
- reduxForm({ form: PROJECT_CREATE_DIALOG }),
- withStyles(styles))(
- class DialogProjectCreate extends React.Component<DialogProjectProps & WithStyles<CssRules>> {
- render() {
- const { classes, open, handleClose, handleSubmit, onSubmit, submitting, invalid, pristine } = this.props;
-
- return (
- <Dialog
- open={open}
- onClose={handleClose}
- disableBackdropClick={true}
- disableEscapeKeyDown={true}>
- <div className={classes.dialog}>
- <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
- <DialogTitle id="form-dialog-title" className={classes.dialogTitle}>Create a
- project</DialogTitle>
- <DialogContent className={classes.formContainer}>
- <Field name="name"
- component={TextField}
- validate={PROJECT_NAME_VALIDATION}
- label="Project Name"/>
- <Field name="description"
- component={TextField}
- validate={PROJECT_DESCRIPTION_VALIDATION}
- label="Description - optional"/>
- </DialogContent>
- <DialogActions className={classes.dialogActions}>
- <Button onClick={handleClose} className={classes.button} color="primary"
- disabled={submitting}>CANCEL</Button>
- <Button type="submit"
- className={classes.lastButton}
- color="primary"
- disabled={invalid || submitting || pristine}
- variant="contained">
- CREATE A PROJECT
- </Button>
- {submitting && <CircularProgress size={20} className={classes.createProgress}/>}
- </DialogActions>
- </form>
- </div>
- </Dialog>
- );
- }
- }
-);
+const ProjectAddFields = () => <span>
+ <ProjectNameField />
+ <ProjectDescriptionField />
+</span>;
--- /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 { COLLECTION_COPY_FORM_NAME, CollectionCopyFormDialogData } from '~/store/collections/collection-copy-actions';
+import { DialogCollectionCopy } from "~/views-components/dialog-copy/dialog-collection-copy";
+import { copyCollection } from '~/store/workbench/workbench-actions';
+
+export const CopyCollectionDialog = compose(
+ withDialog(COLLECTION_COPY_FORM_NAME),
+ reduxForm<CollectionCopyFormDialogData>({
+ form: COLLECTION_COPY_FORM_NAME,
+ onSubmit: (data, dispatch) => {
+ dispatch(copyCollection(data));
+ }
+ })
+)(DialogCollectionCopy);
\ 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 { COLLECTION_CREATE_FORM_NAME, CollectionCreateFormDialogData } from '~/store/collections/collection-create-actions';
+import { DialogCollectionCreate } from "~/views-components/dialog-create/dialog-collection-create";
+import { createCollection } from "~/store/workbench/workbench-actions";
+
+export const CreateCollectionDialog = compose(
+ withDialog(COLLECTION_CREATE_FORM_NAME),
+ reduxForm<CollectionCreateFormDialogData>({
+ form: COLLECTION_CREATE_FORM_NAME,
+ onSubmit: (data, dispatch) => {
+ dispatch(createCollection(data));
+ }
+ })
+)(DialogCollectionCreate);
--- /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 { PROJECT_CREATE_FORM_NAME, ProjectCreateFormDialogData } from '~/store/projects/project-create-actions';
+import { DialogProjectCreate } from '~/views-components/dialog-create/dialog-project-create';
+import { createProject } from "~/store/workbench/workbench-actions";
+
+export const CreateProjectDialog = compose(
+ withDialog(PROJECT_CREATE_FORM_NAME),
+ reduxForm<ProjectCreateFormDialogData>({
+ form: PROJECT_CREATE_FORM_NAME,
+ onSubmit: (data, dispatch) => {
+ dispatch(createProject(data));
+ }
+ })
+)(DialogProjectCreate);
\ 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 { CollectionCreateFormDialogData } from '~/store/collections/collection-create-actions';
+import { COLLECTION_UPLOAD_FILES_DIALOG, submitCollectionFiles } from '~/store/collections/collection-upload-actions';
+import { DialogCollectionFilesUpload } from '~/views-components/dialog-upload/dialog-collection-files-upload';
+
+export const FilesUploadCollectionDialog = compose(
+ withDialog(COLLECTION_UPLOAD_FILES_DIALOG),
+ reduxForm<CollectionCreateFormDialogData>({
+ form: COLLECTION_UPLOAD_FILES_DIALOG,
+ onSubmit: (data, dispatch) => {
+ dispatch(submitCollectionFiles());
+ }
+ })
+)(DialogCollectionFilesUpload);
--- /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 { DialogMoveTo } from '~/views-components/dialog-move/dialog-move-to';
+import { COLLECTION_MOVE_FORM_NAME } from '~/store/collections/collection-move-actions';
+import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
+import { moveCollection } from '~/store/workbench/workbench-actions';
+
+export const MoveCollectionDialog = compose(
+ withDialog(COLLECTION_MOVE_FORM_NAME),
+ reduxForm<MoveToFormDialogData>({
+ form: COLLECTION_MOVE_FORM_NAME,
+ onSubmit: (data, dispatch) => {
+ dispatch(moveCollection(data));
+ }
+ })
+)(DialogMoveTo);
--- /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 { PROJECT_MOVE_FORM_NAME } from '~/store/projects/project-move-actions';
+import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
+import { DialogMoveTo } from '~/views-components/dialog-move/dialog-move-to';
+import { moveProject } from '~/store/workbench/workbench-actions';
+
+export const MoveProjectDialog = compose(
+ withDialog(PROJECT_MOVE_FORM_NAME),
+ reduxForm<MoveToFormDialogData>({
+ form: PROJECT_MOVE_FORM_NAME,
+ onSubmit: (data, dispatch) => {
+ dispatch(moveProject(data));
+ }
+ })
+)(DialogMoveTo);
+
--- /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 { CollectionPartialCopyFormData, copyCollectionPartial, COLLECTION_PARTIAL_COPY_FORM_NAME } from '~/store/collections/collection-partial-copy-actions';
+import { DialogCollectionPartialCopy } from "~/views-components/dialog-copy/dialog-collection-partial-copy";
+
+
+export const PartialCopyCollectionDialog = compose(
+ withDialog(COLLECTION_PARTIAL_COPY_FORM_NAME),
+ reduxForm<CollectionPartialCopyFormData>({
+ form: COLLECTION_PARTIAL_COPY_FORM_NAME,
+ onSubmit: (data, dispatch) => {
+ dispatch(copyCollectionPartial(data));
+ }
+ }))(DialogCollectionPartialCopy);
\ 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 { DialogCollectionUpdate } from '~/views-components/dialog-update/dialog-collection-update';
+import { COLLECTION_UPDATE_FORM_NAME, CollectionUpdateFormDialogData } from '~/store/collections/collection-update-actions';
+import { updateCollection } from "~/store/workbench/workbench-actions";
+
+export const UpdateCollectionDialog = compose(
+ withDialog(COLLECTION_UPDATE_FORM_NAME),
+ reduxForm<CollectionUpdateFormDialogData>({
+ form: COLLECTION_UPDATE_FORM_NAME,
+ onSubmit: (data, dispatch) => {
+ dispatch(updateCollection(data));
+ }
+ })
+)(DialogCollectionUpdate);
\ 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 { DialogProjectUpdate } from '~/views-components/dialog-update/dialog-project-update';
+import { PROJECT_UPDATE_FORM_NAME, ProjectUpdateFormDialogData } from '~/store/projects/project-update-actions';
+import { updateProject } from '~/store/workbench/workbench-actions';
+
+export const UpdateProjectDialog = compose(
+ withDialog(PROJECT_UPDATE_FORM_NAME),
+ reduxForm<ProjectUpdateFormDialogData>({
+ form: PROJECT_UPDATE_FORM_NAME,
+ onSubmit: (data, dispatch) => {
+ dispatch(updateProject(data));
+ }
+ })
+)(DialogProjectUpdate);
\ 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, Field } from 'redux-form';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { ProjectTreePickerField } from '~/views-components/project-tree-picker/project-tree-picker';
+import { MOVE_TO_VALIDATION } from '~/validators/validators';
+import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
+
+export const DialogMoveTo = (props: WithDialogProps<string> & InjectedFormProps<MoveToFormDialogData>) =>
+ <FormDialog
+ dialogTitle='Move to'
+ formFields={MoveToDialogFields}
+ submitLabel='Move'
+ {...props}
+ />;
+
+const MoveToDialogFields = () =>
+ <Field
+ name="ownerUuid"
+ component={ProjectTreePickerField}
+ validate={MOVE_TO_VALIDATION} />;
+
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { reduxForm, Field } from 'redux-form';
-import { compose } from 'redux';
-import { ArvadosTheme } from '~/common/custom-theme';
-import { Dialog, DialogActions, DialogContent, DialogTitle, StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress } from '@material-ui/core';
-import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '~/validators/validators';
-import { COLLECTION_FORM_NAME } from '~/store/collections/updater/collection-updater-action';
-import { TextField } from '~/components/text-field/text-field';
-
-type CssRules = 'content' | 'actions' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
-
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- content: {
- display: 'flex',
- flexDirection: 'column'
- },
- actions: {
- margin: 0,
- padding: `${theme.spacing.unit}px ${theme.spacing.unit * 3 - theme.spacing.unit / 2}px
- ${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px`
- },
- buttonWrapper: {
- position: 'relative'
- },
- saveButton: {
- boxShadow: 'none'
- },
- circularProgress: {
- position: 'absolute',
- top: 0,
- bottom: 0,
- left: 0,
- right: 0,
- margin: 'auto'
- }
-});
-
-interface DialogCollectionDataProps {
- open: boolean;
- handleSubmit: any;
- submitting: boolean;
- invalid: boolean;
- pristine: boolean;
-}
-
-interface DialogCollectionAction {
- handleClose: () => void;
- onSubmit: (data: { name: string, description: string }) => void;
-}
-
-type DialogCollectionProps = DialogCollectionDataProps & DialogCollectionAction & WithStyles<CssRules>;
-
-export const DialogCollectionUpdate = compose(
- reduxForm({ form: COLLECTION_FORM_NAME }),
- withStyles(styles))(
-
- class DialogCollectionUpdate extends React.Component<DialogCollectionProps> {
-
- render() {
- const { classes, open, handleClose, handleSubmit, onSubmit, submitting, invalid, pristine } = this.props;
- return (
- <Dialog open={open}
- onClose={handleClose}
- fullWidth={true}
- maxWidth='sm'
- disableBackdropClick={true}
- disableEscapeKeyDown={true}>
-
- <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
- <DialogTitle>Edit Collection</DialogTitle>
- <DialogContent className={classes.content}>
- <Field name='name'
- disabled={submitting}
- component={TextField}
- validate={COLLECTION_NAME_VALIDATION}
- label="Collection Name" />
- <Field name='description'
- disabled={submitting}
- component={TextField}
- validate={COLLECTION_DESCRIPTION_VALIDATION}
- label="Description - optional" />
- </DialogContent>
- <DialogActions className={classes.actions}>
- <Button onClick={handleClose} color="primary"
- disabled={submitting}>CANCEL</Button>
- <div className={classes.buttonWrapper}>
- <Button type="submit" className={classes.saveButton}
- color="primary"
- disabled={invalid || submitting || pristine}
- variant="contained">
- SAVE
- </Button>
- {submitting && <CircularProgress size={20} className={classes.circularProgress} />}
- </div>
- </DialogActions>
- </form>
- </Dialog>
- );
- }
- }
- );
+import { InjectedFormProps } from 'redux-form';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { CollectionUpdateFormDialogData } from '~/store/collections/collection-update-actions';
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { CollectionNameField, CollectionDescriptionField } from '~/views-components/form-fields/collection-form-fields';
+
+type DialogCollectionProps = WithDialogProps<{}> & InjectedFormProps<CollectionUpdateFormDialogData>;
+
+export const DialogCollectionUpdate = (props: DialogCollectionProps) =>
+ <FormDialog
+ dialogTitle='Edit Collection'
+ formFields={CollectionEditFields}
+ submitLabel='Save'
+ {...props}
+ />;
+
+const CollectionEditFields = () => <span>
+ <CollectionNameField />
+ <CollectionDescriptionField />
+</span>;
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { reduxForm, Field } from 'redux-form';
-import { compose } from 'redux';
-import { ArvadosTheme } from '~/common/custom-theme';
-import { StyleRulesCallback, WithStyles, withStyles, Dialog, DialogTitle, DialogContent, DialogActions, CircularProgress, Button } from '../../../node_modules/@material-ui/core';
-import { TextField } from '~/components/text-field/text-field';
-import { PROJECT_FORM_NAME } from '~/store/project/project-action';
-import { PROJECT_NAME_VALIDATION, PROJECT_DESCRIPTION_VALIDATION } from '~/validators/validators';
-
-type CssRules = 'content' | 'actions' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
-
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- content: {
- display: 'flex',
- flexDirection: 'column'
- },
- actions: {
- margin: 0,
- padding: `${theme.spacing.unit}px ${theme.spacing.unit * 3 - theme.spacing.unit / 2}px
- ${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px`
- },
- buttonWrapper: {
- position: 'relative'
- },
- saveButton: {
- boxShadow: 'none'
- },
- circularProgress: {
- position: 'absolute',
- top: 0,
- bottom: 0,
- left: 0,
- right: 0,
- margin: 'auto'
- }
-});
-
-interface DialogProjectDataProps {
- open: boolean;
- handleSubmit: any;
- submitting: boolean;
- invalid: boolean;
- pristine: boolean;
-}
-
-interface DialogProjectActionProps {
- handleClose: () => void;
- onSubmit: (data: { name: string, description: string }) => void;
-}
-
-type DialogProjectProps = DialogProjectDataProps & DialogProjectActionProps & WithStyles<CssRules>;
-
-export const DialogProjectUpdate = compose(
- reduxForm({ form: PROJECT_FORM_NAME }),
- withStyles(styles))(
-
- class DialogProjectUpdate extends React.Component<DialogProjectProps> {
- render() {
- const { handleSubmit, handleClose, onSubmit, open, classes, submitting, invalid, pristine } = this.props;
- return <Dialog open={open}
- onClose={handleClose}
- fullWidth={true}
- maxWidth='sm'
- disableBackdropClick={true}
- disableEscapeKeyDown={true}>
- <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
- <DialogTitle>Edit Collection</DialogTitle>
- <DialogContent className={classes.content}>
- <Field name='name'
- disabled={submitting}
- component={TextField}
- validate={PROJECT_NAME_VALIDATION}
- label="Project Name" />
- <Field name='description'
- disabled={submitting}
- component={TextField}
- validate={PROJECT_DESCRIPTION_VALIDATION}
- label="Description - optional" />
- </DialogContent>
- <DialogActions className={classes.actions}>
- <Button onClick={handleClose} color="primary"
- disabled={submitting}>CANCEL</Button>
- <div className={classes.buttonWrapper}>
- <Button type="submit" className={classes.saveButton}
- color="primary"
- disabled={invalid || submitting || pristine}
- variant="contained">
- SAVE
- </Button>
- {submitting && <CircularProgress size={20} className={classes.circularProgress} />}
- </div>
- </DialogActions>
- </form>
- </Dialog>;
- }
- }
- );
+import { InjectedFormProps } from 'redux-form';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { ProjectUpdateFormDialogData } from '~/store/projects/project-update-actions';
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { ProjectNameField, ProjectDescriptionField } from '~/views-components/form-fields/project-form-fields';
+
+type DialogProjectProps = WithDialogProps<{}> & InjectedFormProps<ProjectUpdateFormDialogData>;
+
+export const DialogProjectUpdate = (props: DialogProjectProps) =>
+ <FormDialog
+ dialogTitle='Edit Project'
+ formFields={ProjectEditFields}
+ submitLabel='Save'
+ {...props}
+ />;
+
+const ProjectEditFields = () => <span>
+ <ProjectNameField />
+ <ProjectDescriptionField />
+</span>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { InjectedFormProps, Field } from 'redux-form';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { CollectionCreateFormDialogData } from '~/store/collections/collection-create-actions';
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { require } from '~/validators/require';
+import { FileUploaderField } from '~/views-components/file-uploader/file-uploader';
+
+
+type DialogCollectionFilesUploadProps = WithDialogProps<{}> & InjectedFormProps<CollectionCreateFormDialogData>;
+
+export const DialogCollectionFilesUpload = (props: DialogCollectionFilesUploadProps) =>
+ <FormDialog
+ dialogTitle='Upload data'
+ formFields={UploadCollectionFilesFields}
+ submitLabel='Upload data'
+ {...props}
+ />;
+
+const UploadCollectionFilesFields = () =>
+ <Field
+ name='files'
+ validate={FILES_FIELD_VALIDATION}
+ component={FileUploaderField} />;
+
+const FILES_FIELD_VALIDATION = [require];
+
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { FileUpload } from '~/components/file-upload/file-upload';
+import { connect } from 'react-redux';
+import { RootState } from '~/store/store';
+import { FileUploadProps } from '../../components/file-upload/file-upload';
+import { Dispatch } from 'redux';
+import { fileUploaderActions } from '~/store/file-uploader/file-uploader-actions';
+import { WrappedFieldProps } from 'redux-form';
+import { Typography } from '@material-ui/core';
+
+export type FileUploaderProps = Pick<FileUploadProps, 'disabled' | 'onDrop'>;
+
+const mapStateToProps = (state: RootState, { disabled }: FileUploaderProps): Pick<FileUploadProps, 'files' | 'disabled'> => ({
+ disabled,
+ files: state.fileUploader,
+});
+
+const mapDispatchToProps = (dispatch: Dispatch, { onDrop }: FileUploaderProps): Pick<FileUploadProps, 'onDrop'> => ({
+ onDrop: files => {
+ dispatch(fileUploaderActions.SET_UPLOAD_FILES(files));
+ onDrop(files);
+ },
+});
+
+export const FileUploader = connect(mapStateToProps, mapDispatchToProps)(FileUpload);
+
+export const FileUploaderField = (props: WrappedFieldProps & { label?: string }) =>
+ <div>
+ <Typography variant='caption'>{props.label}</Typography>
+ <FileUploader disabled={props.meta.submitting} onDrop={props.input.onChange} />
+ </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 { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION, COLLECTION_PROJECT_VALIDATION } from "~/validators/validators";
+import { ProjectTreePicker } from "~/views-components/project-tree-picker/project-tree-picker";
+
+export const CollectionNameField = () =>
+ <Field
+ name='name'
+ component={TextField}
+ validate={COLLECTION_NAME_VALIDATION}
+ label="Collection Name" />;
+
+export const CollectionDescriptionField = () =>
+ <Field
+ name='description'
+ component={TextField}
+ validate={COLLECTION_DESCRIPTION_VALIDATION}
+ label="Description - optional" />;
+
+export const CollectionProjectPickerField = () =>
+ <Field
+ name="projectUuid"
+ component={ProjectPicker}
+ validate={COLLECTION_PROJECT_VALIDATION} />;
+
+const ProjectPicker = (props: WrappedFieldProps) =>
+ <div style={{ width: '400px', 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 } from "redux-form";
+import { TextField } from "~/components/text-field/text-field";
+import { PROJECT_NAME_VALIDATION, PROJECT_DESCRIPTION_VALIDATION } from "~/validators/validators";
+
+export const ProjectNameField = () =>
+ <Field
+ name='name'
+ component={TextField}
+ validate={PROJECT_NAME_VALIDATION}
+ label="Project Name" />;
+
+export const ProjectDescriptionField = () =>
+ <Field
+ name='description'
+ component={TextField}
+ validate={PROJECT_DESCRIPTION_VALIDATION}
+ label="Description - optional" />;
\ No newline at end of file
import * as React from "react";
import { mount, configure } from "enzyme";
import * as Adapter from "enzyme-adapter-react-16";
-import { MainAppBar } from "./main-app-bar";
+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";
it("renders all components and the menu for authenticated user if user prop has value", () => {
const mainAppBar = mount(
<MainAppBar
- user={user}
- onContextMenu={jest.fn()}
- onDetailsPanelToggle={jest.fn()}
- {...{ searchText: "", breadcrumbs: [], menuItems: { accountMenu: [], helpMenu: [], anonymousMenu: [] }, onSearch: jest.fn(), onBreadcrumbClick: jest.fn(), onMenuItemClick: jest.fn() }}
+ {...mockMainAppBarProps({ user })}
/>
);
expect(mainAppBar.find(SearchBar)).toHaveLength(1);
const menuItems = { accountMenu: [], helpMenu: [], anonymousMenu: [{ label: 'Sign in' }] };
const mainAppBar = mount(
<MainAppBar
- menuItems={menuItems}
- onDetailsPanelToggle={jest.fn()}
- onContextMenu={jest.fn()}
- {...{ searchText: "", breadcrumbs: [], onSearch: jest.fn(), onBreadcrumbClick: jest.fn(), onMenuItemClick: jest.fn() }}
+ {...mockMainAppBarProps({ user: undefined, menuItems })}
/>
);
expect(mainAppBar.find(SearchBar)).toHaveLength(0);
const onSearch = jest.fn();
const mainAppBar = mount(
<MainAppBar
- searchText="search text"
- searchDebounce={2000}
- onContextMenu={jest.fn()}
- onSearch={onSearch}
- onDetailsPanelToggle={jest.fn()}
- {...{ user, breadcrumbs: [], menuItems: { accountMenu: [], helpMenu: [], anonymousMenu: [] }, onBreadcrumbClick: jest.fn(), onMenuItemClick: jest.fn() }}
+ {...mockMainAppBarProps({ searchText: 'search text', searchDebounce: 2000, onSearch, user })}
/>
);
const searchBar = mainAppBar.find(SearchBar);
expect(onSearch).toBeCalledWith("new search text");
});
- it("communicates with <Breadcrumbs />", () => {
- const items = [{ label: "breadcrumb 1" }];
- const onBreadcrumbClick = jest.fn();
- const mainAppBar = mount(
- <MainAppBar
- breadcrumbs={items}
- onContextMenu={jest.fn()}
- onBreadcrumbClick={onBreadcrumbClick}
- onDetailsPanelToggle={jest.fn()}
- {...{ user, searchText: "", menuItems: { accountMenu: [], helpMenu: [], anonymousMenu: [] }, onSearch: jest.fn(), onMenuItemClick: jest.fn() }}
- />
- );
- const breadcrumbs = mainAppBar.find(Breadcrumbs);
- expect(breadcrumbs.prop("items")).toBe(items);
- breadcrumbs.prop("onClick")(items[0]);
- expect(onBreadcrumbClick).toBeCalledWith(items[0]);
- });
-
it("communicates with menu", () => {
const onMenuItemClick = jest.fn();
- const menuItems = { accountMenu: [{label: "log out"}], helpMenu: [], anonymousMenu: [] };
+ const menuItems = { accountMenu: [{ label: "log out" }], helpMenu: [], anonymousMenu: [] };
const mainAppBar = mount(
<MainAppBar
- menuItems={menuItems}
- onContextMenu={jest.fn()}
- onMenuItemClick={onMenuItemClick}
- onDetailsPanelToggle={jest.fn()}
- {...{ user, searchText: "", breadcrumbs: [], onSearch: jest.fn(), onBreadcrumbClick: jest.fn() }}
+ {...mockMainAppBarProps({ menuItems, onMenuItemClick, user })}
/>
);
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,
+});
import { AppBar, Toolbar, Typography, Grid, IconButton, Badge, Button, MenuItem } from "@material-ui/core";
import { User, getUserFullname } from "~/models/user";
import { SearchBar } from "~/components/search-bar/search-bar";
-import { Breadcrumbs, Breadcrumb } from "~/components/breadcrumbs/breadcrumbs";
import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
import { DetailsIcon, NotificationIcon, UserPanelIcon, HelpIcon } from "~/components/icon/icon";
interface MainAppBarDataProps {
searchText: string;
searchDebounce?: number;
- breadcrumbs: Breadcrumb[];
+ breadcrumbs: React.ComponentType<any>;
user?: User;
menuItems: MainAppBarMenuItems;
buildInfo: string;
export interface MainAppBarActionProps {
onSearch: (searchText: string) => void;
- onBreadcrumbClick: (breadcrumb: Breadcrumb) => void;
onMenuItemClick: (menuItem: MainAppBarMenuItem) => void;
- onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: Breadcrumb) => void;
onDetailsPanelToggle: () => void;
}
-type MainAppBarProps = MainAppBarDataProps & MainAppBarActionProps;
+export type MainAppBarProps = MainAppBarDataProps & MainAppBarActionProps;
export const MainAppBar: React.SFC<MainAppBarProps> = (props) => {
return <AppBar position="static">
</Grid>
</Toolbar>
<Toolbar >
- {
- props.user && <Breadcrumbs
- items={props.breadcrumbs}
- onClick={props.onBreadcrumbClick}
- onContextMenu={props.onContextMenu} />
- }
- { props.user && <IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
- <DetailsIcon />
- </IconButton>
+ {props.user && <props.breadcrumbs />}
+ {props.user && <IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
+ <DetailsIcon />
+ </IconButton>
}
</Toolbar>
</AppBar>;
import { Dispatch } from "redux";
import { connect } from "react-redux";
import { Typography } from "@material-ui/core";
-import { TreePicker } from "../tree-picker/tree-picker";
-import { TreeProps, TreeItem, TreeItemStatus } from "~/components/tree/tree";
+import { TreePicker, TreePickerProps } from "../tree-picker/tree-picker";
+import { TreeItem, TreeItemStatus } from "~/components/tree/tree";
import { ProjectResource } from "~/models/project";
import { treePickerActions } from "~/store/tree-picker/tree-picker-actions";
import { ListItemTextIcon } from "~/components/list-item-text-icon/list-item-text-icon";
-import { ProjectIcon } from "~/components/icon/icon";
+import { ProjectIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon } from "~/components/icon/icon";
import { createTreePickerNode } from "~/store/tree-picker/tree-picker";
import { RootState } from "~/store/store";
import { ServiceRepository } from "~/services/services";
import { FilterBuilder } from "~/common/api/filter-builder";
+import { WrappedFieldProps } from 'redux-form';
-type ProjectTreePickerProps = Pick<TreeProps<ProjectResource>, 'toggleItemActive' | 'toggleItemOpen'>;
+type ProjectTreePickerProps = Pick<TreePickerProps, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen'>;
-const mapDispatchToProps = (dispatch: Dispatch, props: {onChange: (projectUuid: string) => void}): ProjectTreePickerProps => ({
- toggleItemActive: id => {
- dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ id }));
- props.onChange(id);
+const mapDispatchToProps = (dispatch: Dispatch, props: { onChange: (projectUuid: string) => void }): ProjectTreePickerProps => ({
+ onContextMenu: () => { return; },
+ toggleItemActive: (nodeId, status, pickerId) => {
+ getNotSelectedTreePickerKind(pickerId)
+ .forEach(pickerId => dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId: '', pickerId })));
+ dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, pickerId }));
+
+ props.onChange(nodeId);
},
- toggleItemOpen: (id, status) => {
- status === TreeItemStatus.INITIAL
- ? dispatch<any>(loadProjectTreePickerProjects(id))
- : dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id }));
+ toggleItemOpen: (nodeId, status, pickerId) => {
+ dispatch<any>(toggleItemOpen(nodeId, status, pickerId));
}
});
+const toggleItemOpen = (nodeId: string, status: TreeItemStatus, pickerId: string) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ if (status === TreeItemStatus.INITIAL) {
+ if (pickerId === TreePickerId.PROJECTS) {
+ dispatch<any>(loadProjectTreePickerProjects(nodeId));
+ } else if (pickerId === TreePickerId.FAVORITES) {
+ dispatch<any>(loadFavoriteTreePickerProjects(nodeId === services.authService.getUuid() ? '' : nodeId));
+ } else {
+ // TODO: load sharedWithMe
+ }
+ } else {
+ dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId }));
+ }
+ };
+
+const getNotSelectedTreePickerKind = (pickerId: string) => {
+ return [TreePickerId.PROJECTS, TreePickerId.FAVORITES, TreePickerId.SHARED_WITH_ME].filter(nodeId => nodeId !== pickerId);
+};
+
+export enum TreePickerId {
+ PROJECTS = 'Projects',
+ SHARED_WITH_ME = 'Shared with me',
+ FAVORITES = 'Favorites'
+}
+
export const ProjectTreePicker = connect(undefined, mapDispatchToProps)((props: ProjectTreePickerProps) =>
- <div style={{display: 'flex', flexDirection: 'column'}}>
- <Typography variant='caption' style={{flexShrink: 0}}>
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
+ <Typography variant='caption' style={{ flexShrink: 0 }}>
Select a project
</Typography>
- <div style={{flexGrow: 1, overflow: 'auto'}}>
- <TreePicker {...props} render={renderTreeItem} />
+ <div style={{ flexGrow: 1, overflow: 'auto' }}>
+ <TreePicker {...props} render={renderTreeItem} pickerId={TreePickerId.PROJECTS} />
+ <TreePicker {...props} render={renderTreeItem} pickerId={TreePickerId.SHARED_WITH_ME} />
+ <TreePicker {...props} render={renderTreeItem} pickerId={TreePickerId.FAVORITES} />
</div>
</div>);
+
// TODO: move action creator to store directory
-export const loadProjectTreePickerProjects = (id: string) =>
+export const loadProjectTreePickerProjects = (nodeId: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id }));
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId, pickerId: TreePickerId.PROJECTS }));
- const ownerUuid = id.length === 0 ? services.authService.getUuid() || '' : id;
+ const ownerUuid = nodeId.length === 0 ? services.authService.getUuid() || '' : nodeId;
const filters = new FilterBuilder()
.addEqual('ownerUuid', ownerUuid)
const { items } = await services.projectService.list({ filters });
- dispatch<any>(receiveProjectTreePickerData(id, items));
+ dispatch<any>(receiveTreePickerData(nodeId, items, TreePickerId.PROJECTS));
};
+export const loadFavoriteTreePickerProjects = (nodeId: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const parentId = services.authService.getUuid() || '';
+
+ if (nodeId === '') {
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId: parentId, pickerId: TreePickerId.FAVORITES }));
+ const { items } = await services.favoriteService.list(parentId);
+
+ dispatch<any>(receiveTreePickerData(parentId, items as ProjectResource[], TreePickerId.FAVORITES));
+ } else {
+ dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId, pickerId: TreePickerId.FAVORITES }));
+ const filters = new FilterBuilder()
+ .addEqual('ownerUuid', nodeId)
+ .getFilters();
+
+ const { items } = await services.projectService.list({ filters });
+
+ dispatch<any>(receiveTreePickerData(nodeId, items, TreePickerId.FAVORITES));
+ }
+
+ };
+
+const getProjectPickerIcon = (item: TreeItem<ProjectResource>) => {
+ switch (item.data.name) {
+ case TreePickerId.FAVORITES:
+ return FavoriteIcon;
+ case TreePickerId.PROJECTS:
+ return ProjectsIcon;
+ case TreePickerId.SHARED_WITH_ME:
+ return ShareMeIcon;
+ default:
+ return ProjectIcon;
+ }
+};
+
const renderTreeItem = (item: TreeItem<ProjectResource>) =>
<ListItemTextIcon
- icon={ProjectIcon}
+ icon={getProjectPickerIcon(item)}
name={item.data.name}
isActive={item.active}
hasMargin={true} />;
+
// TODO: move action creator to store directory
-const receiveProjectTreePickerData = (id: string, projects: ProjectResource[]) =>
+export const receiveTreePickerData = (nodeId: string, projects: ProjectResource[], pickerId: string) =>
(dispatch: Dispatch) => {
dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
- id,
- nodes: projects.map(project => createTreePickerNode({ id: project.uuid, value: project }))
+ nodeId,
+ nodes: projects.map(project => createTreePickerNode({ nodeId: project.uuid, value: project })),
+ pickerId,
}));
- dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id }));
+
+ dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId }));
};
+
+export const ProjectTreePickerField = (props: WrappedFieldProps) =>
+ <div style={{ height: '200px', display: 'flex', flexDirection: 'column' }}>
+ <ProjectTreePicker onChange={handleChange(props)} />
+ {props.meta.dirty && props.meta.error &&
+ <Typography variant='caption' color='error'>
+ {props.meta.error}
+ </Typography>}
+ </div>;
+
+const handleChange = (props: WrappedFieldProps) => (value: string) =>
+ props.input.value === value
+ ? props.input.onChange('')
+ : props.input.onChange(value);
+
import CircularProgress from '@material-ui/core/CircularProgress';
import { ProjectTree } from './project-tree';
-import { TreeItem } from '~/components/tree/tree';
-import { ProjectResource } from '~/models/project';
-import { mockProjectResource } from '~/models/test-utils';
+import { TreeItem, TreeItemStatus } from '../../components/tree/tree';
+import { ProjectResource } from '../../models/project';
+import { mockProjectResource } from '../../models/test-utils';
Enzyme.configure({ adapter: new Adapter() });
id: "3",
open: true,
active: true,
- status: 1
+ status: TreeItemStatus.PENDING
};
const wrapper = mount(<ProjectTree
projects={[project]}
id: "3",
open: true,
active: true,
- status: 2,
+ status: TreeItemStatus.LOADED,
items: [
{
data: mockProjectResource(),
id: "3",
open: true,
active: true,
- status: 1
+ status: TreeItemStatus.PENDING
}
]
}
id: "3",
open: false,
active: true,
- status: 1
+ status: TreeItemStatus.PENDING
};
const wrapper = mount(<ProjectTree
projects={[project]}
//
// SPDX-License-Identifier: AGPL-3.0
-import { Dispatch } from "redux";
-import { reduxForm, reset, startSubmit, stopSubmit } from "redux-form";
-import { withDialog } from "~/store/dialog/with-dialog";
-import { dialogActions } from "~/store/dialog/dialog-actions";
-import { RenameDialog } from "~/components/rename-dialog/rename-dialog";
+import * as React from 'react';
+import { compose } from 'redux';
+import { reduxForm, InjectedFormProps, Field } from 'redux-form';
+import { withDialog, WithDialogProps } from '~/store/dialog/with-dialog';
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { DialogContentText } from '@material-ui/core';
+import { TextField } from '~/components/text-field/text-field';
+import { RENAME_FILE_DIALOG, RenameFileDialogData, renameFile } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions';
-export const RENAME_FILE_DIALOG = 'renameFileDialog';
-
-export const openRenameFileDialog = (originalName: string) =>
- (dispatch: Dispatch) => {
- dispatch(reset(RENAME_FILE_DIALOG));
- dispatch(dialogActions.OPEN_DIALOG({ id: RENAME_FILE_DIALOG, data: originalName }));
- };
-
-export const [RenameFileDialog] = [RenameDialog]
- .map(withDialog(RENAME_FILE_DIALOG))
- .map(reduxForm({
+export const RenameFileDialog = compose(
+ withDialog(RENAME_FILE_DIALOG),
+ reduxForm({
form: RENAME_FILE_DIALOG,
- onSubmit: (data, dispatch) => {
- dispatch(startSubmit(RENAME_FILE_DIALOG));
- // TODO: call collection file renaming action here
- setTimeout(() => dispatch(stopSubmit(RENAME_FILE_DIALOG, { name: 'Invalid name' })), 2000);
+ onSubmit: (data: { name: string }, dispatch) => {
+ dispatch<any>(renameFile(data.name));
}
- }));
+ })
+)((props: WithDialogProps<RenameFileDialogData> & InjectedFormProps<{ name: string }>) =>
+ <FormDialog
+ dialogTitle='Rename'
+ formFields={RenameDialogFormFields}
+ submitLabel='Ok'
+ {...props}
+ />);
+
+const RenameDialogFormFields = (props: WithDialogProps<RenameFileDialogData>) => <>
+ <DialogContentText>
+ {`Please, enter a new name for ${props.data.name}`}
+ </DialogContentText>
+ <Field
+ name='name'
+ component={TextField}
+ />
+</>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dispatch } from "redux";
+import { connect } from "react-redux";
+import { TreePicker, TreePickerProps } from "../tree-picker/tree-picker";
+import { TreeItem } from "~/components/tree/tree";
+import { ProjectResource } from "~/models/project";
+import { ListItemTextIcon } from "~/components/list-item-text-icon/list-item-text-icon";
+import { ProjectIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, TrashIcon } from '~/components/icon/icon';
+import { RecentIcon, WorkflowIcon } from '~/components/icon/icon';
+import { activateSidePanelTreeItem, toggleSidePanelTreeItemCollapse, SIDE_PANEL_TREE, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
+import { openSidePanelContextMenu } from '~/store/context-menu/context-menu-actions';
+
+export interface SidePanelTreeProps {
+ onItemActivation: (id: string) => void;
+}
+
+type SidePanelTreeActionProps = Pick<TreePickerProps, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen'>;
+
+const mapDispatchToProps = (dispatch: Dispatch, props: SidePanelTreeProps): SidePanelTreeActionProps => ({
+ onContextMenu: (event, id) => {
+ dispatch<any>(openSidePanelContextMenu(event, id));
+ },
+ toggleItemActive: (nodeId) => {
+ dispatch<any>(activateSidePanelTreeItem(nodeId));
+ props.onItemActivation(nodeId);
+ },
+ toggleItemOpen: (nodeId) => {
+ dispatch<any>(toggleSidePanelTreeItemCollapse(nodeId));
+ }
+});
+
+export const SidePanelTree = connect(undefined, mapDispatchToProps)(
+ (props: SidePanelTreeActionProps) =>
+ <TreePicker {...props} render={renderSidePanelItem} pickerId={SIDE_PANEL_TREE} />);
+
+const renderSidePanelItem = (item: TreeItem<ProjectResource>) =>
+ <ListItemTextIcon
+ icon={getProjectPickerIcon(item)}
+ name={typeof item.data === 'string' ? item.data : item.data.name}
+ isActive={item.active}
+ hasMargin={true} />;
+
+const getProjectPickerIcon = (item: TreeItem<ProjectResource | string>) =>
+ typeof item.data === 'string'
+ ? getSidePanelIcon(item.data)
+ : ProjectIcon;
+
+const getSidePanelIcon = (category: string) => {
+ switch (category) {
+ case SidePanelTreeCategory.FAVORITES:
+ return FavoriteIcon;
+ case SidePanelTreeCategory.PROJECTS:
+ return ProjectsIcon;
+ case SidePanelTreeCategory.RECENT_OPEN:
+ return RecentIcon;
+ case SidePanelTreeCategory.SHARED_WITH_ME:
+ return ShareMeIcon;
+ case SidePanelTreeCategory.TRASH:
+ return TrashIcon;
+ case SidePanelTreeCategory.WORKFLOWS:
+ return WorkflowIcon;
+ default:
+ return ProjectIcon;
+ }
+};
--- /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/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';
+
+const DRAWER_WITDH = 240;
+
+type CssRules = 'drawerPaper' | 'toolbar';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ drawerPaper: {
+ position: 'relative',
+ width: DRAWER_WITDH,
+ display: 'flex',
+ flexDirection: 'column',
+ paddingTop: 58,
+ overflow: 'auto',
+ },
+ toolbar: theme.mixins.toolbar
+});
+
+const mapDispatchToProps = (dispatch: Dispatch): SidePanelTreeProps => ({
+ onItemActivation: id => {
+ dispatch<any>(navigateFromSidePanel(id));
+ }
+});
+
+export const SidePanel = compose(
+ withStyles(styles),
+ connect(undefined, mapDispatchToProps)
+)(({ classes, ...props }: WithStyles<CssRules> & SidePanelTreeProps) =>
+ <Drawer
+ variant="permanent"
+ classes={{ paper: classes.drawerPaper }}>
+ <div className={classes.toolbar} />
+ <SidePanelTree {...props} />
+ </Drawer>);
// SPDX-License-Identifier: AGPL-3.0
import { connect } from "react-redux";
-import { Tree, TreeProps, TreeItem } from "~/components/tree/tree";
+import { Tree, TreeProps, TreeItem, TreeItemStatus } from "~/components/tree/tree";
import { RootState } from "~/store/store";
-import { TreePicker as TTreePicker, TreePickerNode, createTreePickerNode } from "~/store/tree-picker/tree-picker";
-import { getNodeValue, getNodeChildren } from "~/models/tree";
+import { createTreePickerNode, TreePickerNode } from "~/store/tree-picker/tree-picker";
+import { getNodeValue, getNodeChildrenIds, Tree as Ttree, createTree } from "~/models/tree";
+import { Dispatch } from "redux";
-const memoizedMapStateToProps = () => {
- let prevState: TTreePicker;
- let prevTree: Array<TreeItem<any>>;
+export interface TreePickerProps {
+ pickerId: string;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, nodeId: string, pickerId: string) => void;
+ toggleItemOpen: (nodeId: string, status: TreeItemStatus, pickerId: string) => void;
+ toggleItemActive: (nodeId: string, status: TreeItemStatus, pickerId: string) => void;
+}
- return (state: RootState): Pick<TreeProps<any>, 'items'> => {
- if (prevState !== state.treePicker) {
- prevState = state.treePicker;
- prevTree = getNodeChildren('')(state.treePicker)
- .map(treePickerToTreeItems(state.treePicker));
+const memoizedMapStateToProps = () => {
+ let prevTree: Ttree<TreePickerNode>;
+ let mappedProps: Pick<TreeProps<any>, 'items'>;
+ return (state: RootState, props: TreePickerProps): Pick<TreeProps<any>, 'items'> => {
+ const tree = state.treePicker[props.pickerId] || createTree();
+ if(tree !== prevTree){
+ prevTree = tree;
+ mappedProps = {
+ items: getNodeChildrenIds('')(tree)
+ .map(treePickerToTreeItems(tree))
+ };
}
- return {
- items: prevTree
- };
+ return mappedProps;
};
};
-const mapDispatchToProps = (): Pick<TreeProps<any>, 'onContextMenu'> => ({
- onContextMenu: () => { return; },
+const mapDispatchToProps = (dispatch: Dispatch, props: TreePickerProps): Pick<TreeProps<any>, 'onContextMenu' | 'toggleItemOpen' | 'toggleItemActive'> => ({
+ onContextMenu: (event, item) => props.onContextMenu(event, item.id, props.pickerId),
+ toggleItemActive: (id, status) => props.toggleItemActive(id, status, props.pickerId),
+ toggleItemOpen: (id, status) => props.toggleItemOpen(id, status, props.pickerId)
});
export const TreePicker = connect(memoizedMapStateToProps(), mapDispatchToProps)(Tree);
-const treePickerToTreeItems = (tree: TTreePicker) =>
+const treePickerToTreeItems = (tree: Ttree<TreePickerNode>) =>
(id: string): TreeItem<any> => {
- const node: TreePickerNode = getNodeValue(id)(tree) || createTreePickerNode({ id: '', value: 'InvalidNode' });
- const items = getNodeChildren(node.id)(tree)
+ const node: TreePickerNode = getNodeValue(id)(tree) || createTreePickerNode({ nodeId: '', value: 'InvalidNode' });
+ const items = getNodeChildrenIds(node.nodeId)(tree)
.map(treePickerToTreeItems(tree));
return {
active: node.selected,
data: node.value,
- id: node.id,
+ id: node.nodeId,
items: items.length > 0 ? items : undefined,
open: !node.collapsed,
status: node.status
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { connect } from "react-redux";
-import { Dispatch } from "redux";
-import { SubmissionError } from "redux-form";
-import { RootState } from "~/store/store";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
-import { collectionUpdaterActions, updateCollection } from "~/store/collections/updater/collection-updater-action";
-import { dataExplorerActions } from "~/store/data-explorer/data-explorer-action";
-import { PROJECT_PANEL_ID } from "~/views/project-panel/project-panel";
-import { DialogCollectionUpdate } from "../dialog-update/dialog-collection-update";
-
-const mapStateToProps = (state: RootState) => ({
- open: state.collections.updater.opened
-});
-
-const mapDispatchToProps = (dispatch: Dispatch) => ({
- handleClose: () => {
- dispatch(collectionUpdaterActions.CLOSE_COLLECTION_UPDATER());
- },
- onSubmit: (data: { name: string, description: string }) => {
- return dispatch<any>(editCollection(data))
- .catch((e: any) => {
- if(e.errors) {
- throw new SubmissionError({ name: e.errors.join("").includes("UniqueViolation") ? "Collection with this name already exists." : "" });
- }
- });
- }
-});
-
-const editCollection = (data: { name: string, description: string }) =>
- (dispatch: Dispatch) => {
- return dispatch<any>(updateCollection(data)).then(() => {
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Collection has been successfully updated.",
- hideDuration: 2000
- }));
- dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
- });
- };
-
-export const UpdateCollectionDialog = connect(mapStateToProps, mapDispatchToProps)(DialogCollectionUpdate);
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { connect } from "react-redux";
-import { Dispatch } from "redux";
-import { SubmissionError } from "redux-form";
-import { RootState } from "~/store/store";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
-import { DialogProjectUpdate } from "../dialog-update/dialog-project-update";
-import { projectActions, updateProject } from "~/store/project/project-action";
-
-const mapStateToProps = (state: RootState) => ({
- open: state.projects.updater.opened
-});
-
-const mapDispatchToProps = (dispatch: Dispatch) => ({
- handleClose: () => {
- dispatch(projectActions.CLOSE_PROJECT_UPDATER());
- },
- onSubmit: (data: { name: string, description: string }) => {
- return dispatch<any>(editProject(data))
- .catch((e: any) => {
- if (e.errors) {
- throw new SubmissionError({ name: e.errors.join("").includes("UniqueViolation") ? "Project with this name already exists." : "" });
- }
- });
- }
-});
-
-const editProject = (data: { name: string, description: string }) =>
- (dispatch: Dispatch, getState: () => RootState) => {
- const { uuid } = getState().projects.updater;
- return dispatch<any>(updateProject(data)).then(() => {
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Project has been successfully updated.",
- hideDuration: 2000
- }));
- });
- };
-
-export const UpdateProjectDialog = connect(mapStateToProps, mapDispatchToProps)(DialogProjectUpdate);
import * as React from 'react';
import {
StyleRulesCallback, WithStyles, withStyles, Card,
- CardHeader, IconButton, CardContent, Grid, Chip
+ CardHeader, IconButton, CardContent, Grid, Chip, Tooltip
} from '@material-ui/core';
import { connect, DispatchProp } from "react-redux";
import { RouteComponentProps } from 'react-router';
import { TagResource } from '~/models/tag';
import { CollectionTagForm } from './collection-tag-form';
import { deleteCollectionTag } from '~/store/collection-panel/collection-panel-action';
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { getResource } from '~/store/resources/resources';
+import { contextMenuActions, openContextMenu } from '~/store/context-menu/context-menu-actions';
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
-type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'value';
+type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'label' | 'value';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
card: {
color: theme.palette.grey["500"],
cursor: 'pointer'
},
+ label: {
+ fontSize: '0.875rem'
+ },
value: {
- textTransform: 'none'
+ textTransform: 'none',
+ fontSize: '0.875rem'
}
});
tags: TagResource[];
}
-interface CollectionPanelActionProps {
- onItemRouteChange: (collectionId: string) => void;
- onContextMenu: (event: React.MouseEvent<HTMLElement>, item: CollectionResource) => void;
-}
-
-type CollectionPanelProps = CollectionPanelDataProps & CollectionPanelActionProps & DispatchProp
- & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+type CollectionPanelProps = CollectionPanelDataProps & DispatchProp
+ & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
export const CollectionPanel = withStyles(styles)(
- connect((state: RootState) => ({
- item: state.collectionPanel.item,
- tags: state.collectionPanel.tags
- }))(
+ connect((state: RootState, props: RouteComponentProps<{ id: string }>) => {
+ const collection = getResource(props.match.params.id)(state.resources);
+ return {
+ item: collection,
+ tags: state.collectionPanel.tags
+ };
+ })(
class extends React.Component<CollectionPanelProps> {
render() {
- const { classes, item, tags, onContextMenu } = this.props;
+ const { classes, item, tags } = this.props;
return <div>
- <Card className={classes.card}>
- <CardHeader
- avatar={ <CollectionIcon className={classes.iconHeader} /> }
- action={
- <IconButton
- aria-label="More options"
- onClick={event => onContextMenu(event, item)}>
- <MoreOptionsIcon />
- </IconButton>
- }
- title={item && item.name }
- subheader={item && item.description} />
- <CardContent>
- <Grid container direction="column">
- <Grid item xs={6}>
- <DetailsAttribute classValue={classes.value}
- label='Collection UUID'
- value={item && item.uuid}>
- <CopyToClipboard text={item && item.uuid}>
- <CopyIcon className={classes.copyIcon} />
- </CopyToClipboard>
+ <Card className={classes.card}>
+ <CardHeader
+ avatar={<CollectionIcon className={classes.iconHeader} />}
+ action={
+ <IconButton
+ aria-label="More options"
+ onClick={this.handleContextMenu}>
+ <MoreOptionsIcon />
+ </IconButton>
+ }
+ title={item && item.name}
+ subheader={item && item.description} />
+ <CardContent>
+ <Grid container direction="column">
+ <Grid item xs={6}>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Collection UUID'
+ value={item && item.uuid}>
+ <Tooltip title="Copy uuid">
+ <CopyToClipboard text={item && item.uuid} onCopy={() => this.onCopy()}>
+ <CopyIcon className={classes.copyIcon} />
+ </CopyToClipboard>
+ </Tooltip>
</DetailsAttribute>
- <DetailsAttribute label='Number of files' value='14' />
- <DetailsAttribute label='Content size' value='54 MB' />
- <DetailsAttribute classValue={classes.value} label='Owner' value={item && item.ownerUuid} />
- </Grid>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Number of files' value='14' />
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Content size' value='54 MB' />
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Owner' value={item && item.ownerUuid} />
</Grid>
- </CardContent>
- </Card>
+ </Grid>
+ </CardContent>
+ </Card>
- <Card className={classes.card}>
- <CardHeader title="Properties" />
- <CardContent>
- <Grid container direction="column">
- <Grid item xs={12}><CollectionTagForm /></Grid>
- <Grid item xs={12}>
- {
- tags.map(tag => {
- return <Chip key={tag.etag} className={classes.tag}
- onDelete={this.handleDelete(tag.uuid)}
- label={renderTagLabel(tag)} />;
- })
- }
- </Grid>
+ <Card className={classes.card}>
+ <CardHeader title="Properties" />
+ <CardContent>
+ <Grid container direction="column">
+ <Grid item xs={12}><CollectionTagForm /></Grid>
+ <Grid item xs={12}>
+ {
+ tags.map(tag => {
+ return <Chip key={tag.etag} className={classes.tag}
+ onDelete={this.handleDelete(tag.uuid)}
+ label={renderTagLabel(tag)} />;
+ })
+ }
</Grid>
- </CardContent>
- </Card>
- <div className={classes.card}>
- <CollectionPanelFiles/>
- </div>
- </div>;
+ </Grid>
+ </CardContent>
+ </Card>
+ <div className={classes.card}>
+ <CollectionPanelFiles />
+ </div>
+ </div>;
+ }
+
+ handleContextMenu = (event: React.MouseEvent<any>) => {
+ const { uuid, name, description } = this.props.item;
+ const resource = {
+ uuid,
+ name,
+ description,
+ kind: ContextMenuKind.COLLECTION
+ };
+ this.props.dispatch<any>(openContextMenu(event, resource));
}
handleDelete = (uuid: string) => () => {
this.props.dispatch<any>(deleteCollectionTag(uuid));
}
- componentWillReceiveProps({ match, item, onItemRouteChange }: CollectionPanelProps) {
- if (!item || match.params.id !== item.uuid) {
- onItemRouteChange(match.params.id);
- }
+ onCopy = () => {
+ this.props.dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Uuid has been copied",
+ hideDuration: 2000
+ }));
}
}
)
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { GroupContentsResource } from "~/services/groups-service/groups-service";
-import { ResourceKind } from "~/models/resource";
-
-export interface FavoritePanelItem {
- uuid: string;
- name: string;
- kind: string;
- url: string;
- owner: string;
- lastModified: string;
- fileSize?: number;
- status?: string;
- isTrashed?: boolean;
-}
-
-export function resourceToDataItem(r: GroupContentsResource): FavoritePanelItem {
- return {
- uuid: r.uuid,
- name: r.name,
- kind: r.kind,
- url: "",
- owner: r.ownerUuid,
- lastModified: r.modifiedAt,
- status: r.kind === ResourceKind.PROCESS ? r.state : undefined,
- isTrashed: r.kind === ResourceKind.GROUP || r.kind === ResourceKind.COLLECTION ? r.isTrashed: undefined
- };
-}
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { FavoritePanelItem } from './favorite-panel-item';
import { 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 { RouteComponentProps } from 'react-router';
-import { RootState } from '~/store/store';
import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
import { ProcessState } from '~/models/process';
import { SortDirection } from '~/components/data-table/data-column';
import { ResourceKind } from '~/models/resource';
import { resourceLabel } from '~/common/labels';
import { ArvadosTheme } from '~/common/custom-theme';
-import { renderName, renderStatus, renderType, renderOwner, renderFileSize, renderDate } from '~/views-components/data-explorer/renderers';
import { FAVORITE_PANEL_ID } from "~/store/favorite-panel/favorite-panel-action";
+import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner, ResourceName } from '~/views-components/data-explorer/renderers';
import { FavoriteIcon } from '~/components/icon/icon';
+import { Dispatch } from 'redux';
+import { contextMenuActions, openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+import { loadDetailsPanel } from '../../store/details-panel/details-panel-action';
+import { navigateTo } from '~/store/navigation/navigation-action';
type CssRules = "toolbar" | "button";
type: ResourceKind | ProcessState;
}
-export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
+export const favoritePanelColumns: DataColumns<string, FavoritePanelFilter> = [
{
name: FavoritePanelColumnNames.NAME,
selected: true,
configurable: true,
sortDirection: SortDirection.ASC,
filters: [],
- render: renderName,
+ render: uuid => <ResourceName uuid={uuid} />,
width: "450px"
},
{
type: ProcessState.UNCOMMITTED
}
],
- render: renderStatus,
+ render: uuid => <ProcessStatus uuid={uuid} />,
width: "75px"
},
{
type: ResourceKind.PROJECT
}
],
- render: item => renderType(item.kind),
+ render: uuid => <ResourceType uuid={uuid} />,
width: "125px"
},
{
configurable: true,
sortDirection: SortDirection.NONE,
filters: [],
- render: item => renderOwner(item.owner),
+ render: uuid => <ResourceOwner uuid={uuid} />,
width: "200px"
},
{
configurable: true,
sortDirection: SortDirection.NONE,
filters: [],
- render: item => renderFileSize(item.fileSize),
+ render: uuid => <ResourceFileSize uuid={uuid} />,
width: "50px"
},
{
configurable: true,
sortDirection: SortDirection.NONE,
filters: [],
- render: item => renderDate(item.lastModified),
+ render: uuid => <ResourceLastModifiedDate uuid={uuid} />,
width: "150px"
}
];
}
interface FavoritePanelActionProps {
- onItemClick: (item: FavoritePanelItem) => void;
- onContextMenu: (event: React.MouseEvent<HTMLElement>, item: FavoritePanelItem) => void;
+ onItemClick: (item: string) => void;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
onDialogOpen: (ownerUuid: string) => void;
- onItemDoubleClick: (item: FavoritePanelItem) => void;
- onItemRouteChange: (itemId: string) => void;
+ onItemDoubleClick: (item: string) => void;
}
+const mapDispatchToProps = (dispatch: Dispatch): FavoritePanelActionProps => ({
+ onContextMenu: (event, resourceUuid) => {
+ const kind = resourceKindToContextMenuKind(resourceUuid);
+ if (kind) {
+ dispatch<any>(openContextMenu(event, { name: '', uuid: resourceUuid, kind }));
+ }
+ },
+ onDialogOpen: (ownerUuid: string) => { return; },
+ onItemClick: (resourceUuid: string) => {
+ dispatch<any>(loadDetailsPanel(resourceUuid));
+ },
+ onItemDoubleClick: uuid => {
+ dispatch<any>(navigateTo(uuid));
+ }
+});
+
type FavoritePanelProps = FavoritePanelDataProps & FavoritePanelActionProps & DispatchProp
- & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+ & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
export const FavoritePanel = withStyles(styles)(
- connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))(
+ connect(undefined, mapDispatchToProps)(
class extends React.Component<FavoritePanelProps> {
render() {
return <DataExplorer
id={FAVORITE_PANEL_ID}
- columns={columns}
onRowClick={this.props.onItemClick}
onRowDoubleClick={this.props.onItemDoubleClick}
onContextMenu={this.props.onContextMenu}
- extractKey={(item: FavoritePanelItem) => item.uuid}
defaultIcon={FavoriteIcon}
- defaultMessages={['Your favorites list is empty.']}/>
- ;
- }
-
- componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: FavoritePanelProps) {
- if (match.params.id !== currentItemId) {
- onItemRouteChange(match.params.id);
- }
+ defaultMessages={['Your favorites list is empty.']} />;
}
}
)
--- /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, Card,
+ CardHeader, IconButton, CardContent, Grid, Chip, Typography, Tooltip
+} from '@material-ui/core';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { MoreOptionsIcon, ProcessIcon } from '~/components/icon/icon';
+import { DetailsAttribute } from '~/components/details-attribute/details-attribute';
+import { Process } from '~/store/processes/process';
+import { getProcessStatus } from '../../store/processes/process';
+
+type CssRules = 'card' | 'iconHeader' | 'label' | 'value' | 'chip' | 'headerText' | 'link' | 'content' | 'title' | 'avatar';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ card: {
+ marginBottom: theme.spacing.unit * 2
+ },
+ iconHeader: {
+ fontSize: '1.875rem',
+ color: theme.customs.colors.green700,
+ },
+ avatar: {
+ alignSelf: 'flex-start'
+ },
+ label: {
+ display: 'flex',
+ justifyContent: 'flex-end',
+ fontSize: '0.875rem',
+ marginRight: theme.spacing.unit * 3,
+ paddingRight: theme.spacing.unit
+ },
+ value: {
+ textTransform: 'none',
+ fontSize: '0.875rem',
+ },
+ link: {
+ fontSize: '0.875rem',
+ color: theme.palette.primary.main,
+ '&:hover': {
+ cursor: 'pointer'
+ }
+ },
+ chip: {
+ height: theme.spacing.unit * 3,
+ width: theme.spacing.unit * 12,
+ backgroundColor: theme.customs.colors.green700,
+ color: theme.palette.common.white,
+ fontSize: '0.875rem',
+ borderRadius: theme.spacing.unit * 0.625,
+ },
+ headerText: {
+ fontSize: '0.875rem',
+ marginLeft: theme.spacing.unit * 3,
+ },
+ content: {
+ '&:last-child': {
+ paddingBottom: theme.spacing.unit * 2,
+ paddingTop: '0px'
+ }
+ },
+ title: {
+ overflow: 'hidden'
+ }
+});
+
+export interface ProcessInformationCardDataProps {
+ process: Process;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
+}
+
+type ProcessInformationCardProps = ProcessInformationCardDataProps & WithStyles<CssRules>;
+
+export const ProcessInformationCard = withStyles(styles)(
+ ({ classes, process, onContextMenu }: ProcessInformationCardProps) =>
+ <Card className={classes.card}>
+ <CardHeader
+ classes={{
+ content: classes.title,
+ avatar: classes.avatar
+ }}
+ avatar={<ProcessIcon className={classes.iconHeader} />}
+ action={
+ <div>
+ <Chip label={getProcessStatus(process)} className={classes.chip} />
+ <IconButton
+ aria-label="More options"
+ onClick={event => onContextMenu(event)}>
+ <MoreOptionsIcon />
+ </IconButton>
+ </div>
+ }
+ title={
+ <Tooltip title={process.containerRequest.name}>
+ <Typography noWrap variant="title">
+ {process.containerRequest.name}
+ </Typography>
+ </Tooltip>
+ }
+ subheader={process.containerRequest.description} />
+ <CardContent className={classes.content}>
+ <Grid container>
+ <Grid item xs={6}>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='From' value={process.container ? process.container.startedAt : 'N/A'} />
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='To' value={process.container ? process.container.finishedAt : 'N/A'} />
+ <DetailsAttribute classLabel={classes.label} classValue={classes.link}
+ label='Workflow' value='???' />
+ </Grid>
+ <Grid item xs={6}>
+ <DetailsAttribute classLabel={classes.link} label='Outputs' />
+ <DetailsAttribute classLabel={classes.link} label='Inputs' />
+ </Grid>
+ </Grid>
+ </CardContent>
+ </Card>
+);
\ 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 { Grid } from '@material-ui/core';
+import { ProcessInformationCard } from './process-information-card';
+import { DefaultView } from '~/components/default-view/default-view';
+import { ProcessIcon } from '~/components/icon/icon';
+import { Process } from '~/store/processes/process';
+import { SubprocessesCard } from './subprocesses-card';
+
+export interface ProcessPanelRootDataProps {
+ process?: Process;
+}
+
+export interface ProcessPanelRootActionProps {
+ onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
+}
+
+export type ProcessPanelRootProps = ProcessPanelRootDataProps & ProcessPanelRootActionProps;
+
+export const ProcessPanelRoot = (props: ProcessPanelRootProps) =>
+ props.process
+ ? <Grid container spacing={16}>
+ <Grid item xs={7}>
+ <ProcessInformationCard
+ process={props.process}
+ onContextMenu={props.onContextMenu} />
+ </Grid>
+ <Grid item xs={5}>
+ <SubprocessesCard
+ subprocesses={4}
+ filters={[
+ {
+ key: 'queued',
+ value: 1,
+ label: 'Queued',
+ checked: true
+ }, {
+ key: 'active',
+ value: 2,
+ label: 'Active',
+ checked: true
+ },
+ {
+ key: 'completed',
+ value: 2,
+ label: 'Completed',
+ checked: true
+ },
+ {
+ key: 'failed',
+ value: 2,
+ label: 'Failed',
+ checked: true
+ }
+ ]}
+ onToggle={() => { return; }}
+ />
+ </Grid>
+ </Grid>
+ : <Grid container
+ alignItems='center'
+ justify='center'>
+ <DefaultView
+ icon={ProcessIcon}
+ messages={['Process not found']} />
+ </Grid>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// 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 { matchProcessRoute } from '~/routes/routes';
+import { ProcessPanelRootDataProps, ProcessPanelRootActionProps, ProcessPanelRoot } from './process-panel-root';
+
+const mapStateToProps = ({ router, resources }: RootState): ProcessPanelRootDataProps => {
+ const pathname = router.location ? router.location.pathname : '';
+ const match = matchProcessRoute(pathname);
+ const uuid = match ? match.params.id : '';
+ return {
+ process: getProcess(uuid)(resources)
+ };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch): ProcessPanelRootActionProps => ({
+ onContextMenu: (event: React.MouseEvent<HTMLElement>) => {
+ dispatch<any>(openProcessContextMenu(event));
+ }
+});
+
+export const ProcessPanel = connect(mapStateToProps, mapDispatchToProps)(ProcessPanelRoot);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { StyleRulesCallback, withStyles, WithStyles, Card, CardHeader, CardContent, Grid, Switch } from '@material-ui/core';
+import { SubprocessFilter } from '~/components/subprocess-filter/subprocess-filter';
+import { SubprocessFilterDataProps } from '~/components/subprocess-filter/subprocess-filter';
+
+type CssRules = 'root';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ fontSize: '0.875rem'
+ }
+});
+
+interface SubprocessesDataProps {
+ subprocesses: number;
+ filters: SubprocessFilterDataProps[];
+ onToggle: (filter: SubprocessFilterDataProps) => void;
+}
+
+type SubprocessesProps = SubprocessesDataProps & WithStyles<CssRules>;
+
+export const SubprocessesCard = withStyles(styles)(
+ ({ classes, filters, subprocesses, onToggle }: SubprocessesProps) =>
+ <Card className={classes.root}>
+ <CardHeader title="Subprocess and filters" />
+ <CardContent>
+ <Grid container direction="column" spacing={16}>
+ <Grid item xs={12} container spacing={16}>
+ <SubprocessFilter label='Subprocesses' value={subprocesses} />
+ </Grid>
+ <Grid item xs={12} container spacing={16}>
+ {
+ filters.map(filter =>
+ <SubprocessFilter {...filter} key={filter.key} onToggle={() => onToggle(filter)} />
+ )
+ }
+ </Grid>
+ </Grid>
+ </CardContent>
+ </Card>
+);
\ No newline at end of file
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { GroupContentsResource } from "~/services/groups-service/groups-service";
-import { ResourceKind } from "~/models/resource";
-
-export interface ProjectPanelItem {
- uuid: string;
- name: string;
- description?: string;
- kind: string;
- url: string;
- owner: string;
- lastModified: string;
- fileSize?: number;
- status?: string;
- isTrashed?: boolean;
-}
-
-export function resourceToDataItem(r: GroupContentsResource): ProjectPanelItem {
- return {
- uuid: r.uuid,
- name: r.name,
- description: r.description,
- kind: r.kind,
- url: "",
- owner: r.ownerUuid,
- lastModified: r.modifiedAt,
- status: r.kind === ResourceKind.PROCESS ? r.state : undefined,
- isTrashed: r.kind === ResourceKind.GROUP || r.kind === ResourceKind.COLLECTION ? r.isTrashed: undefined
- };
-}
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { ProjectPanelItem } from './project-panel-item';
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 { ResourceKind } from '~/models/resource';
import { resourceLabel } from '~/common/labels';
import { ArvadosTheme } from '~/common/custom-theme';
-import { renderName, renderStatus, renderType, renderOwner, renderFileSize, renderDate } from '~/views-components/data-explorer/renderers';
-import { restoreBranch } from '~/store/navigation/navigation-action';
+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 { ResourcesState, getResource } from '~/store/resources/resources';
+import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+import { contextMenuActions, resourceKindToContextMenuKind, openContextMenu } from '~/store/context-menu/context-menu-actions';
+import { CollectionResource } from '~/models/collection';
+import { ProjectResource } from '~/models/project';
+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';
type CssRules = 'root' | "toolbar" | "button";
type: ResourceKind | ProcessState;
}
-export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
+export const projectPanelColumns: DataColumns<string, ProjectPanelFilter> = [
{
name: ProjectPanelColumnNames.NAME,
selected: true,
configurable: true,
sortDirection: SortDirection.ASC,
filters: [],
- render: renderName,
+ render: uuid => <ResourceName uuid={uuid} />,
width: "450px"
},
{
type: ProcessState.UNCOMMITTED
}
],
- render: renderStatus,
+ render: uuid => <ProcessStatus uuid={uuid} />,
width: "75px"
},
{
type: ResourceKind.PROJECT
}
],
- render: item => renderType(item.kind),
+ render: uuid => <ResourceType uuid={uuid} />,
width: "125px"
},
{
configurable: true,
sortDirection: SortDirection.NONE,
filters: [],
- render: item => renderOwner(item.owner),
+ render: uuid => <ResourceOwner uuid={uuid} />,
width: "200px"
},
{
configurable: true,
sortDirection: SortDirection.NONE,
filters: [],
- render: item => renderFileSize(item.fileSize),
+ render: uuid => <ResourceFileSize uuid={uuid} />,
width: "50px"
},
{
configurable: true,
sortDirection: SortDirection.NONE,
filters: [],
- render: item => renderDate(item.lastModified),
+ render: uuid => <ResourceLastModifiedDate uuid={uuid} />,
width: "150px"
}
];
interface ProjectPanelDataProps {
currentItemId: string;
+ resources: ResourcesState;
}
-interface ProjectPanelActionProps {
- onItemClick: (item: ProjectPanelItem) => void;
- onContextMenu: (event: React.MouseEvent<HTMLElement>, item: ProjectPanelItem) => void;
- onProjectCreationDialogOpen: (ownerUuid: string) => void;
- onCollectionCreationDialogOpen: (ownerUuid: string) => void;
- onItemDoubleClick: (item: ProjectPanelItem) => void;
- onItemRouteChange: (itemId: string) => void;
-}
-
-type ProjectPanelProps = ProjectPanelDataProps & ProjectPanelActionProps & DispatchProp
+type ProjectPanelProps = ProjectPanelDataProps & DispatchProp
& WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
export const ProjectPanel = withStyles(styles)(
- connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))(
+ connect((state: RootState) => ({
+ currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
+ resources: state.resources
+ }))(
class extends React.Component<ProjectPanelProps> {
render() {
const { classes } = this.props;
</div>
<DataExplorer
id={PROJECT_PANEL_ID}
- columns={columns}
- onRowClick={this.props.onItemClick}
- onRowDoubleClick={this.props.onItemDoubleClick}
- onContextMenu={this.props.onContextMenu}
- extractKey={(item: ProjectPanelItem) => item.uuid}
+ onRowClick={this.handleRowClick}
+ onRowDoubleClick={this.handleRowDoubleClick}
+ onContextMenu={this.handleContextMenu}
defaultIcon={ProjectIcon}
defaultMessages={['Your project is empty.', 'Please create a project or create a collection and upload a data.']} />
</div>;
}
handleNewProjectClick = () => {
- this.props.onProjectCreationDialogOpen(this.props.currentItemId);
+ this.props.dispatch<any>(openProjectCreateDialog(this.props.currentItemId));
}
handleNewCollectionClick = () => {
- this.props.onCollectionCreationDialogOpen(this.props.currentItemId);
+ this.props.dispatch<any>(openCollectionCreateDialog(this.props.currentItemId));
}
- componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: ProjectPanelProps) {
- if (match.params.id !== currentItemId) {
- onItemRouteChange(match.params.id);
+ handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+ const kind = resourceKindToContextMenuKind(resourceUuid);
+ if (kind) {
+ this.props.dispatch<any>(openContextMenu(event, { name: '', uuid: resourceUuid, kind }));
}
}
- componentDidMount() {
- if (this.props.match.params.id && this.props.currentItemId === '') {
- this.props.dispatch<any>(restoreBranch(this.props.match.params.id));
- }
+ handleRowDoubleClick = (uuid: string) => {
+ this.props.dispatch<any>(navigateTo(uuid));
}
+
+ handleRowClick = (uuid: string) => {
+ this.props.dispatch(loadDetailsPanel(uuid));
+ }
+
}
)
);
import * as React from 'react';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import Drawer from '@material-ui/core/Drawer';
import { connect, DispatchProp } from "react-redux";
-import { Route, RouteComponentProps, Switch, Redirect } from "react-router";
+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 { Breadcrumb } from '~/components/breadcrumbs/breadcrumbs';
import { push } from 'react-router-redux';
-import { reset } from 'redux-form';
-import { ProjectTree } from '~/views-components/project-tree/project-tree';
-import { TreeItem } from "~/components/tree/tree";
-import { getTreePath } from '~/store/project/project-reducer';
-import { sidePanelActions } from '~/store/side-panel/side-panel-action';
-import { SidePanel, SidePanelItem } from '~/components/side-panel/side-panel';
-import { ItemMode, setProjectItem } from "~/store/navigation/navigation-action";
-import { projectActions } from "~/store/project/project-action";
-import { collectionCreateActions } from '~/store/collections/creator/collection-creator-action';
import { ProjectPanel } from "~/views/project-panel/project-panel";
import { DetailsPanel } from '~/views-components/details-panel/details-panel';
import { ArvadosTheme } from '~/common/custom-theme';
-import { CreateProjectDialog } from "~/views-components/create-project-dialog/create-project-dialog";
-
-import { detailsPanelActions, loadDetails } from "~/store/details-panel/details-panel-action";
-import { contextMenuActions } from "~/store/context-menu/context-menu-actions";
-import { ProjectResource } from '~/models/project';
-import { ResourceKind } from '~/models/resource';
-import { ContextMenu, ContextMenuKind } from "~/views-components/context-menu/context-menu";
+import { detailsPanelActions } from "~/store/details-panel/details-panel-action";
+import { ContextMenu } from "~/views-components/context-menu/context-menu";
import { FavoritePanel } from "../favorite-panel/favorite-panel";
import { CurrentTokenDialog } from '~/views-components/current-token-dialog/current-token-dialog';
import { Snackbar } from '~/views-components/snackbar/snackbar';
-import { favoritePanelActions } from '~/store/favorite-panel/favorite-panel-action';
-import { CreateCollectionDialog } from '~/views-components/create-collection-dialog/create-collection-dialog';
import { CollectionPanel } from '../collection-panel/collection-panel';
-import { loadCollection, loadCollectionTags } from '~/store/collection-panel/collection-panel-action';
-import { getCollectionUrl } from '~/models/collection';
-import { UpdateCollectionDialog } from '~/views-components/update-collection-dialog/update-collection-dialog.';
-import { UpdateProjectDialog } from '~/views-components/update-project-dialog/update-project-dialog';
import { AuthService } from "~/services/auth-service/auth-service";
import { RenameFileDialog } from '~/views-components/rename-file-dialog/rename-file-dialog';
import { FileRemoveDialog } from '~/views-components/file-remove-dialog/file-remove-dialog';
import { MultipleFilesRemoveDialog } from '~/views-components/file-remove-dialog/multiple-files-remove-dialog';
-import { DialogCollectionCreateWithSelectedFile } from '~/views-components/create-collection-dialog-with-selected/create-collection-dialog-with-selected';
-import { COLLECTION_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-collection-create';
-import { PROJECT_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-project-create';
+import { Routes } from '~/routes/routes';
+import { SidePanel } from '~/views-components/side-panel/side-panel';
+import { ProcessPanel } from '~/views/process-panel/process-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 { UpdateCollectionDialog } from '~/views-components/dialog-forms/update-collection-dialog';
+import { UpdateProjectDialog } from '~/views-components/dialog-forms/update-project-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 { trashPanelActions } from "~/store/trash-panel/trash-panel-action";
-const DRAWER_WITDH = 240;
const APP_BAR_HEIGHT = 100;
-type CssRules = 'root' | 'appBar' | 'drawerPaper' | 'content' | 'contentWrapper' | 'toolbar';
+type CssRules = 'root' | 'appBar' | 'content' | 'contentWrapper';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
position: "absolute",
width: "100%"
},
- drawerPaper: {
- position: 'relative',
- width: DRAWER_WITDH,
- display: 'flex',
- flexDirection: 'column',
- },
contentWrapper: {
backgroundColor: theme.palette.background.default,
display: "flex",
flexGrow: 1,
position: 'relative'
},
- toolbar: theme.mixins.toolbar
});
interface WorkbenchDataProps {
- projects: Array<TreeItem<ProjectResource>>;
- currentProjectId: string;
user?: User;
currentToken?: string;
- sidePanelItems: SidePanelItem[];
}
interface WorkbenchGeneralProps {
type WorkbenchProps = WorkbenchDataProps & WorkbenchGeneralProps & WorkbenchActionProps & DispatchProp<any> & WithStyles<CssRules>;
-interface NavBreadcrumb extends Breadcrumb {
- itemId: string;
-}
-
interface NavMenuItem extends MainAppBarMenuItem {
action: () => void;
}
export const Workbench = withStyles(styles)(
connect<WorkbenchDataProps>(
(state: RootState) => ({
- projects: state.projects.items,
- currentProjectId: state.projects.currentItemId,
user: state.auth.user,
currentToken: state.auth.apiToken,
- sidePanelItems: state.sidePanel
})
)(
class extends React.Component<WorkbenchProps, WorkbenchState> {
state = {
- isCreationDialogOpen: false,
isCurrentTokenDialogOpen: false,
anchorEl: null,
searchText: "",
};
render() {
- const path = getTreePath(this.props.projects, this.props.currentProjectId);
- const breadcrumbs = path.map(item => ({
- label: item.data.name,
- itemId: item.data.uuid,
- status: item.status
- }));
-
const { classes, user } = this.props;
return (
<div className={classes.root}>
<div className={classes.appBar}>
<MainAppBar
- breadcrumbs={breadcrumbs}
+ breadcrumbs={Breadcrumbs}
searchText={this.state.searchText}
user={this.props.user}
menuItems={this.state.menuItems}
buildInfo={this.props.buildInfo}
{...this.mainAppBarActions} />
</div>
- {user &&
- <Drawer
- variant="permanent"
- classes={{
- paper: classes.drawerPaper,
- }}>
- <div className={classes.toolbar} />
- <SidePanel
- toggleOpen={this.toggleSidePanelOpen}
- toggleActive={this.toggleSidePanelActive}
- sidePanelItems={this.props.sidePanelItems}
- onContextMenu={(event) => this.openContextMenu(event, {
- uuid: this.props.authService.getUuid() || "",
- name: "",
- kind: ContextMenuKind.ROOT_PROJECT
- })}>
- <ProjectTree
- projects={this.props.projects}
- toggleOpen={itemId => this.props.dispatch(setProjectItem(itemId, ItemMode.OPEN))}
- onContextMenu={(event, item) => this.openContextMenu(event, {
- uuid: item.data.uuid,
- ownerUuid: item.data.ownerUuid || this.props.authService.getUuid(),
- isTrashed: item.data.isTrashed,
- name: item.data.name,
- kind: ContextMenuKind.PROJECT
- })}
- toggleActive={itemId => {
- this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE));
- this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT));
- }} />
- </SidePanel>
- </Drawer>}
+ {user && <SidePanel />}
<main className={classes.contentWrapper}>
<div className={classes.content}>
<Switch>
- <Route path='/' exact render={() => <Redirect to={`/projects/${this.props.authService.getUuid()}`} />} />
- <Route path="/projects/:id" render={this.renderProjectPanel} />
- <Route path="/favorites" render={this.renderFavoritePanel} />
+ <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="/trash" render={this.renderTrashPanel} />
- <Route path="/collections/:id" render={this.renderCollectionPanel} />
</Switch>
</div>
{user && <DetailsPanel />}
<CreateProjectDialog />
<CreateCollectionDialog />
<RenameFileDialog />
- <DialogCollectionCreateWithSelectedFile />
+ <PartialCopyCollectionDialog />
+ <FileRemoveDialog />
+ <CopyCollectionDialog />
<FileRemoveDialog />
<MultipleFilesRemoveDialog />
<UpdateCollectionDialog />
+ <FilesUploadCollectionDialog />
<UpdateProjectDialog />
+ <MoveCollectionDialog />
+ <MoveProjectDialog />
<CurrentTokenDialog
currentToken={this.props.currentToken}
open={this.state.isCurrentTokenDialogOpen}
);
}
- renderCollectionPanel = (props: RouteComponentProps<{ id: string }>) => <CollectionPanel
- onItemRouteChange={(collectionId) => {
- this.props.dispatch<any>(loadCollection(collectionId));
- this.props.dispatch<any>(loadCollectionTags(collectionId));
- }}
- onContextMenu={(event, item) => {
- this.openContextMenu(event, {
- uuid: item.uuid,
- name: item.name,
- description: item.description,
- isTrashed: item.isTrashed,
- kind: ContextMenuKind.COLLECTION
- });
- }}
- {...props} />
-
- renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => <ProjectPanel
- onItemRouteChange={itemId => this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE))}
- onContextMenu={(event, item) => {
- let kind: ContextMenuKind;
-
- if (item.kind === ResourceKind.PROJECT) {
- kind = ContextMenuKind.PROJECT;
- } else if (item.kind === ResourceKind.COLLECTION) {
- kind = ContextMenuKind.COLLECTION_RESOURCE;
- } else {
- kind = ContextMenuKind.RESOURCE;
- }
-
- this.openContextMenu(event, {
- uuid: item.uuid,
- name: item.name,
- description: item.description,
- isTrashed: item.isTrashed,
- ownerUuid: item.owner || this.props.authService.getUuid(),
- kind
- });
- }}
- onProjectCreationDialogOpen={this.handleProjectCreationDialogOpen}
- onCollectionCreationDialogOpen={this.handleCollectionCreationDialogOpen}
- onItemClick={item => {
- this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
- }}
- onItemDoubleClick={item => {
- switch (item.kind) {
- case ResourceKind.COLLECTION:
- this.props.dispatch(loadCollection(item.uuid));
- this.props.dispatch(push(getCollectionUrl(item.uuid)));
- default:
- this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
- this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
- }
-
- }}
- {...props} />
-
- renderFavoritePanel = (props: RouteComponentProps<{ id: string }>) => <FavoritePanel
- onItemRouteChange={() => this.props.dispatch(favoritePanelActions.REQUEST_ITEMS())}
- onContextMenu={(event, item) => {
- const kind = item.kind === ResourceKind.PROJECT ? ContextMenuKind.PROJECT : ContextMenuKind.RESOURCE;
- this.openContextMenu(event, {
- uuid: item.uuid,
- name: item.name,
- isTrashed: item.isTrashed,
- kind,
- });
- }}
- onDialogOpen={this.handleProjectCreationDialogOpen}
- onItemClick={item => {
- this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
- }}
- onItemDoubleClick={item => {
- switch (item.kind) {
- case ResourceKind.COLLECTION:
- this.props.dispatch(loadCollection(item.uuid));
- this.props.dispatch(push(getCollectionUrl(item.uuid)));
- default:
- this.props.dispatch(loadDetails(item.uuid, ResourceKind.PROJECT));
- this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
- }
-
- }}
- {...props} />
-
- renderTrashPanel = (props: RouteComponentProps<{ id: string }>) => <TrashPanel
- onItemRouteChange={() => this.props.dispatch(trashPanelActions.REQUEST_ITEMS())}
- onContextMenu={(event, item) => {
- const kind = item.kind === ResourceKind.PROJECT ? ContextMenuKind.PROJECT : ContextMenuKind.COLLECTION;
- this.openContextMenu(event, {
- uuid: item.uuid,
- name: item.name,
- isTrashed: item.isTrashed,
- ownerUuid: item.owner,
- kind,
- });
- }}
- onDialogOpen={this.handleProjectCreationDialogOpen}
- onItemClick={item => {
- // this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
- }}
- onItemDoubleClick={item => {
- // switch (item.kind) {
- // case ResourceKind.COLLECTION:
- // this.props.dispatch(loadCollection(item.uuid));
- // this.props.dispatch(push(getCollectionUrl(item.uuid)));
- // default:
- // this.props.dispatch(loadDetails(item.uuid, ResourceKind.PROJECT));
- // this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
- // }
-
- }}
- {...props} />
-
mainAppBarActions: MainAppBarActionProps = {
- onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
- this.props.dispatch(setProjectItem(itemId, ItemMode.BOTH));
- this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT));
- },
onSearch: searchText => {
this.setState({ searchText });
this.props.dispatch(push(`/search?q=${searchText}`));
onDetailsPanelToggle: () => {
this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
},
- onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: NavBreadcrumb) => {
- this.openContextMenu(event, {
- uuid: breadcrumb.itemId,
- name: breadcrumb.label,
- kind: ContextMenuKind.PROJECT
- });
- }
};
- toggleSidePanelOpen = (itemId: string) => {
- this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId));
- }
-
- toggleSidePanelActive = (itemId: string) => {
- this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
-
- const panelItem = this.props.sidePanelItems.find(it => it.id === itemId);
- if (panelItem && panelItem.activeAction) {
- panelItem.activeAction(this.props.dispatch, this.props.authService.getUuid());
- }
- }
-
- handleProjectCreationDialogOpen = (itemUuid: string) => {
- this.props.dispatch(reset(PROJECT_CREATE_DIALOG));
- this.props.dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: itemUuid }));
- }
-
- handleCollectionCreationDialogOpen = (itemUuid: string) => {
- this.props.dispatch(reset(COLLECTION_CREATE_DIALOG));
- this.props.dispatch(collectionCreateActions.OPEN_COLLECTION_CREATOR({ ownerUuid: itemUuid }));
- }
-
- openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: { name: string; uuid: string; description?: string; isTrashed?: boolean, ownerUuid?: string, kind: ContextMenuKind; }) => {
- event.preventDefault();
- this.props.dispatch(
- contextMenuActions.OPEN_CONTEXT_MENU({
- position: { x: event.clientX, y: event.clientY },
- resource
- })
- );
- }
-
toggleCurrentTokenModal = () => {
this.setState({ isCurrentTokenDialogOpen: !this.state.isCurrentTokenDialogOpen });
}
esutils "^2.0.2"
js-tokens "^3.0.0"
-"@babel/runtime@^7.0.0-beta.42":
- version "7.0.0-beta.54"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.54.tgz#39ebb42723fe7ca4b3e1b00e967e80138d47cadf"
+"@babel/runtime@7.0.0-beta.42":
+ version "7.0.0-beta.42"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.42.tgz#352e40c92e0460d3e82f49bd7e79f6cda76f919f"
+ dependencies:
+ core-js "^2.5.3"
+ regenerator-runtime "^0.11.1"
+
+"@babel/runtime@7.0.0-beta.56":
+ version "7.0.0-beta.56"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.56.tgz#cda612dffd5b1719a7b8e91e3040bd6ae64de8b0"
dependencies:
- core-js "^2.5.7"
regenerator-runtime "^0.12.0"
-"@material-ui/core@1.4.2":
- version "1.4.2"
- resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-1.4.2.tgz#8a1282e985d4922a4d2b4f7e287d8a716a2fc108"
+"@material-ui/core@1.5.0":
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-1.5.0.tgz#00884bb4139d98786d05a97803d19426d4afa55d"
dependencies:
- "@babel/runtime" "^7.0.0-beta.42"
+ "@babel/runtime" "7.0.0-beta.42"
"@types/jss" "^9.5.3"
"@types/react-transition-group" "^2.0.8"
brcast "^3.0.1"
normalize-scroll-left "^0.1.2"
popper.js "^1.14.1"
prop-types "^15.6.0"
- react-event-listener "^0.6.0"
+ react-event-listener "^0.6.2"
react-jss "^8.1.0"
react-transition-group "^2.2.1"
- recompose "^0.27.0"
+ recompose "^0.28.0"
warning "^4.0.1"
-"@material-ui/icons@2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-2.0.0.tgz#f2c4e80d0cb4bbbd433127781da67d93393535f8"
+"@material-ui/icons@2.0.2":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-2.0.2.tgz#0150c38cda089ef284e9b4a730dfe6e88a0b5de6"
dependencies:
- "@babel/runtime" "^7.0.0-beta.42"
- recompose "^0.27.0"
+ "@babel/runtime" "7.0.0-beta.42"
+ recompose "^0.28.0"
"@types/cheerio@*":
version "0.22.8"
version "2.2.6"
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.6.tgz#dbe8a666156d556ed018e15a4c65f08937c3f628"
-"@types/enzyme-adapter-react-16@1.0.2":
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.2.tgz#15ae37c64d6221a6f4b3a4aacc357cf773859de4"
+"@types/enzyme-adapter-react-16@1.0.3":
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.3.tgz#0cf7025b036694ca8d596fe38f24162e7117acf1"
dependencies:
"@types/enzyme" "*"
-"@types/enzyme@*", "@types/enzyme@3.1.12":
+"@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":
+ version "3.1.13"
+ resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.13.tgz#4bbc5c81fa40c9fc7efee25c4a23cb37119a33ea"
+ dependencies:
+ "@types/cheerio" "*"
+ "@types/react" "*"
+
"@types/history@*":
version "4.6.2"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0"
version "10.5.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.2.tgz#f19f05314d5421fe37e74153254201a7bf00a707"
-"@types/node@10.5.5":
- version "10.5.5"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.5.tgz#8e84d24e896cd77b0d4f73df274027e3149ec2ba"
+"@types/node@10.7.1":
+ version "10.7.1"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.7.1.tgz#b704d7c259aa40ee052eec678758a68d07132a2e"
-"@types/react-copy-to-clipboard@4.2.5":
- version "4.2.5"
- resolved "https://registry.yarnpkg.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-4.2.5.tgz#bda288b4256288676019b75ca86f1714bbd206d4"
+"@types/react-copy-to-clipboard@4.2.6":
+ version "4.2.6"
+ resolved "https://registry.yarnpkg.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-4.2.6.tgz#d1374550dec803f17f26ec71b62783c5737bfc02"
dependencies:
"@types/react" "*"
-"@types/react-dom@16.0.6":
- version "16.0.6"
- resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.6.tgz#f1a65a4e7be8ed5d123f8b3b9eacc913e35a1a3c"
+"@types/react-dom@16.0.7":
+ version "16.0.7"
+ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.7.tgz#54d0f867a76b90597e8432030d297982f25c20ba"
dependencies:
"@types/node" "*"
"@types/react" "*"
-"@types/react-dropzone@4.2.1":
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/@types/react-dropzone/-/react-dropzone-4.2.1.tgz#4a973b63a8a227e263ff4eece053f643220f28fc"
+"@types/react-dropzone@4.2.2":
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/@types/react-dropzone/-/react-dropzone-4.2.2.tgz#af0a2595169700c8ab1114e9096285499beaff40"
dependencies:
"@types/react" "*"
"@types/react" "*"
redux "^3.6.0"
-"@types/redux-form@7.4.4":
- version "7.4.4"
- resolved "https://registry.yarnpkg.com/@types/redux-form/-/redux-form-7.4.4.tgz#2cf62b8eb1dc1b1df95b6b25c2763db196e5c190"
+"@types/redux-form@7.4.5":
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/@types/redux-form/-/redux-form-7.4.5.tgz#fae0fa6cfbc613867093d1e0f6a84db17177305e"
dependencies:
"@types/react" "*"
redux "^3.6.0 || ^4.0.0"
version "0.3.2"
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+array.prototype.flat@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz#812db8f02cad24d3fab65dd67eabe3b8903494a4"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.10.0"
+ function-bind "^1.1.1"
+
arrify@^1.0.0, arrify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
version "2.1.1"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.1.tgz#ae2d5a729477f289d60dd7f96a6314a22dd6c22a"
-attr-accept@^1.0.3:
+attr-accept@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-1.1.3.tgz#48230c79f93790ef2775fcec4f0db0f5db41ca52"
dependencies:
version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
-core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.7:
+core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.3:
version "2.5.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
version "1.1.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
-enzyme-adapter-react-16@1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.1.1.tgz#a8f4278b47e082fbca14f5bfb1ee50ee650717b4"
+enzyme-adapter-react-16@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.2.0.tgz#c6e80f334e0a817873262d7d01ee9e4747e3c97e"
dependencies:
- enzyme-adapter-utils "^1.3.0"
- lodash "^4.17.4"
- object.assign "^4.0.4"
+ enzyme-adapter-utils "^1.5.0"
+ function.prototype.name "^1.1.0"
+ object.assign "^4.1.0"
object.values "^1.0.4"
- prop-types "^15.6.0"
+ prop-types "^15.6.2"
+ react-is "^16.4.2"
react-reconciler "^0.7.0"
react-test-renderer "^16.0.0-0"
-enzyme-adapter-utils@^1.3.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.4.0.tgz#c403b81e8eb9953658569e539780964bdc98de62"
+enzyme-adapter-utils@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.5.0.tgz#a020ab3ae79bb1c85e1d51f48f35e995e0eed810"
dependencies:
+ function.prototype.name "^1.1.0"
object.assign "^4.1.0"
- prop-types "^15.6.0"
+ prop-types "^15.6.2"
-enzyme@3.3.0:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.3.0.tgz#0971abd167f2d4bf3f5bd508229e1c4b6dc50479"
+enzyme@3.4.4:
+ version "3.4.4"
+ resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.4.4.tgz#92c7c6b9e59d4ef0c3d36a75dccc0e41a5c14d21"
dependencies:
+ array.prototype.flat "^1.2.1"
cheerio "^1.0.0-rc.2"
- function.prototype.name "^1.0.3"
- has "^1.0.1"
+ function.prototype.name "^1.1.0"
+ has "^1.0.3"
is-boolean-object "^1.0.0"
- is-callable "^1.1.3"
+ is-callable "^1.1.4"
is-number-object "^1.0.3"
is-string "^1.0.4"
is-subset "^0.1.1"
lodash "^4.17.4"
- object-inspect "^1.5.0"
+ object-inspect "^1.6.0"
object-is "^1.0.1"
object.assign "^4.1.0"
object.entries "^1.0.4"
dependencies:
is-arrayish "^0.2.1"
-es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0:
+es-abstract@^1.10.0, es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165"
dependencies:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
-function.prototype.name@^1.0.3:
+function.prototype.name@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.0.tgz#8bd763cc0af860a859cc5d49384d74b932cd2327"
dependencies:
is-number "^3.0.0"
kind-of "^4.0.0"
-has@^1.0.1:
+has@^1.0.1, has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
dependencies:
dependencies:
builtin-modules "^1.0.0"
-is-callable@^1.1.1, is-callable@^1.1.3:
+is-callable@^1.1.1, is-callable@^1.1.3, is-callable@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
define-property "^0.2.5"
kind-of "^3.0.3"
-object-inspect@^1.5.0:
+object-inspect@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b"
dependencies:
isobject "^3.0.0"
-object.assign@^4.0.4, object.assign@^4.1.0:
+object.assign@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
dependencies:
object-assign "^4.1.1"
prop-types "^15.6.0"
-react-dropzone@4.2.13:
- version "4.2.13"
- resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-4.2.13.tgz#31393c079b4e5ddcc176c095cebc3545d1248b9d"
+react-dropzone@5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-5.0.1.tgz#3ed201215794c0f650c6f25a8311a9d96d35ebb6"
dependencies:
- attr-accept "^1.0.3"
+ attr-accept "^1.1.3"
prop-types "^15.5.7"
react-error-overlay@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4"
-react-event-listener@^0.6.0:
- version "0.6.1"
- resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.6.1.tgz#41c7a80a66b398c27dd511e22712b02f3d4eccca"
+react-event-listener@^0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.6.2.tgz#df405e9578be052b77a76e4c3914686637caecff"
dependencies:
- "@babel/runtime" "^7.0.0-beta.42"
+ "@babel/runtime" "7.0.0-beta.42"
prop-types "^15.6.0"
warning "^4.0.1"
version "16.4.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e"
+react-is@^16.4.2:
+ version "16.4.2"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.2.tgz#84891b56c2b6d9efdee577cc83501dfc5ecead88"
+
react-jss@^8.1.0:
version "8.6.1"
resolved "https://registry.yarnpkg.com/react-jss/-/react-jss-8.6.1.tgz#a06e2e1d2c4d91b4d11befda865e6c07fbd75252"
dependencies:
util.promisify "^1.0.0"
-recompose@^0.27.0:
- version "0.27.1"
- resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.27.1.tgz#1a49e931f183634516633bbb4f4edbfd3f38a7ba"
+recompose@^0.28.0:
+ version "0.28.2"
+ resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.28.2.tgz#19e679227bdf979e0d31b73ffe7ae38c9194f4a7"
dependencies:
- babel-runtime "^6.26.0"
+ "@babel/runtime" "7.0.0-beta.56"
change-emitter "^0.1.2"
fbjs "^0.8.1"
hoist-non-react-statics "^2.3.1"
version "1.4.0"
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
-regenerator-runtime@^0.11.0:
+regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.1:
version "0.11.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"