REACT_APP_ARVADOS_CONFIG_URL=/config.json
REACT_APP_ARVADOS_API_HOST=qr1hi.arvadosapi.com
+REACT_APP_ARVADOS_KEEP_WEB_HOST=download.qr1hi.arvadosapi.com
HTTPS=true
\ No newline at end of file
"react-router": "4.3.1",
"react-router-dom": "4.3.1",
"react-router-redux": "5.0.0-alpha.9",
- "react-scripts-ts": "2.16.0",
+ "react-scripts-ts": "2.17.0",
"redux": "4.0.0",
"redux-thunk": "2.3.0",
"unionize": "2.1.2"
"redux-form": "7.4.2",
"typescript": "3.0.1"
},
- "moduleNameMapper": {
- "^~/(.*)$": "<rootDir>/src/$1"
+ "jest": {
+ "moduleNameMapper": {
+ "^~/(.*)$": "<rootDir>/src/$1"
+ }
}
}
import { CommonResourceService } from "./common-resource-service";
import axios, { AxiosInstance } from "axios";
import MockAdapter from "axios-mock-adapter";
-import { Resource } from "../../models/resource";
+import { Resource } from "~/models/resource";
export const mockResourceService = <R extends Resource, C extends CommonResourceService<R>>(Service: new (client: AxiosInstance) => C) => {
const axiosInstance = axios.create();
// SPDX-License-Identifier: AGPL-3.0
import * as _ from "lodash";
-import { FilterBuilder } from "./filter-builder";
-import { OrderBuilder } from "./order-builder";
import { AxiosInstance, AxiosPromise } from "axios";
-import { Resource } from "../../models/resource";
+import { Resource } from "~/models/resource";
export interface ListArguments {
limit?: number;
offset?: number;
- filters?: FilterBuilder;
- order?: OrderBuilder;
+ filters?: string;
+ order?: string;
select?: string[];
distinct?: boolean;
count?: string;
const { filters, order, ...other } = args;
const params = {
...other,
- filters: filters ? filters.serialize() : undefined,
- order: order ? order.getOrder() : undefined
+ filters: filters ? `[${filters}]` : undefined,
+ order: order ? order : undefined
};
return CommonResourceService.defaultResponse(
this.serverApi
let filters: FilterBuilder;
beforeEach(() => {
- filters = FilterBuilder.create();
+ filters = new FilterBuilder();
});
it("should add 'equal' rule", () => {
expect(
- filters.addEqual("etag", "etagValue").serialize()
- ).toEqual(`[["etag","=","etagValue"]]`);
+ filters.addEqual("etag", "etagValue").getFilters()
+ ).toEqual(`["etag","=","etagValue"]`);
});
it("should add 'like' rule", () => {
expect(
- filters.addLike("etag", "etagValue").serialize()
- ).toEqual(`[["etag","like","%etagValue%"]]`);
+ filters.addLike("etag", "etagValue").getFilters()
+ ).toEqual(`["etag","like","%etagValue%"]`);
});
it("should add 'ilike' rule", () => {
expect(
- filters.addILike("etag", "etagValue").serialize()
- ).toEqual(`[["etag","ilike","%etagValue%"]]`);
+ filters.addILike("etag", "etagValue").getFilters()
+ ).toEqual(`["etag","ilike","%etagValue%"]`);
});
it("should add 'is_a' rule", () => {
expect(
- filters.addIsA("etag", "etagValue").serialize()
- ).toEqual(`[["etag","is_a","etagValue"]]`);
+ filters.addIsA("etag", "etagValue").getFilters()
+ ).toEqual(`["etag","is_a","etagValue"]`);
});
it("should add 'is_a' rule for set", () => {
expect(
- filters.addIsA("etag", ["etagValue1", "etagValue2"]).serialize()
- ).toEqual(`[["etag","is_a",["etagValue1","etagValue2"]]]`);
+ filters.addIsA("etag", ["etagValue1", "etagValue2"]).getFilters()
+ ).toEqual(`["etag","is_a",["etagValue1","etagValue2"]]`);
});
it("should add 'in' rule", () => {
expect(
- filters.addIn("etag", "etagValue").serialize()
- ).toEqual(`[["etag","in","etagValue"]]`);
+ filters.addIn("etag", "etagValue").getFilters()
+ ).toEqual(`["etag","in","etagValue"]`);
});
it("should add 'in' rule for set", () => {
expect(
- filters.addIn("etag", ["etagValue1", "etagValue2"]).serialize()
- ).toEqual(`[["etag","in",["etagValue1","etagValue2"]]]`);
+ filters.addIn("etag", ["etagValue1", "etagValue2"]).getFilters()
+ ).toEqual(`["etag","in",["etagValue1","etagValue2"]]`);
});
it("should add multiple rules", () => {
filters
.addIn("etag", ["etagValue1", "etagValue2"])
.addEqual("href", "hrefValue")
- .serialize()
- ).toEqual(`[["etag","in",["etagValue1","etagValue2"]],["href","=","hrefValue"]]`);
- });
-
- it("should concatenate multiple builders", () => {
- expect(
- filters
- .concat(FilterBuilder.create().addIn("etag", ["etagValue1", "etagValue2"]))
- .concat(FilterBuilder.create().addEqual("href", "hrefValue"))
- .serialize()
- ).toEqual(`[["etag","in",["etagValue1","etagValue2"]],["href","=","hrefValue"]]`);
+ .getFilters()
+ ).toEqual(`["etag","in",["etagValue1","etagValue2"]],["href","=","hrefValue"]`);
});
it("should add attribute prefix", () => {
- expect(FilterBuilder
- .create("myPrefix")
- .addIn("etag", ["etagValue1", "etagValue2"])
- .serialize())
- .toEqual(`[["my_prefix.etag","in",["etagValue1","etagValue2"]]]`);
+ expect(new FilterBuilder()
+ .addIn("etag", ["etagValue1", "etagValue2"], "myPrefix")
+ .getFilters())
+ .toEqual(`["my_prefix.etag","in",["etagValue1","etagValue2"]]`);
});
-
-
-
-
});
import * as _ from "lodash";
-export class FilterBuilder {
- static create(resourcePrefix = "") {
- return new FilterBuilder(resourcePrefix);
- }
-
- constructor(
- private resourcePrefix = "",
- private filters = "") { }
+export function joinFilters(filters0?: string, filters1?: string) {
+ return [filters0, filters1].filter(s => s).join(",");
+}
- public addEqual(field: string, value?: string) {
- return this.addCondition(field, "=", value);
- }
+export class FilterBuilder {
+ constructor(private filters = "") { }
- public addLike(field: string, value?: string) {
- return this.addCondition(field, "like", value, "%", "%");
+ public addEqual(field: string, value?: string, resourcePrefix?: string) {
+ return this.addCondition(field, "=", value, "", "", resourcePrefix );
}
- public addILike(field: string, value?: string) {
- return this.addCondition(field, "ilike", value, "%", "%");
+ public addLike(field: string, value?: string, resourcePrefix?: string) {
+ return this.addCondition(field, "like", value, "%", "%", resourcePrefix);
}
- public addIsA(field: string, value?: string | string[]) {
- return this.addCondition(field, "is_a", value);
+ public addILike(field: string, value?: string, resourcePrefix?: string) {
+ return this.addCondition(field, "ilike", value, "%", "%", resourcePrefix);
}
- public addIn(field: string, value?: string | string[]) {
- return this.addCondition(field, "in", value);
+ public addIsA(field: string, value?: string | string[], resourcePrefix?: string) {
+ return this.addCondition(field, "is_a", value, "", "", resourcePrefix);
}
- public concat(filterBuilder: FilterBuilder) {
- return new FilterBuilder(this.resourcePrefix, this.filters + (this.filters && filterBuilder.filters ? "," : "") + filterBuilder.getFilters());
+ public addIn(field: string, value?: string | string[], resourcePrefix?: string) {
+ return this.addCondition(field, "in", value, "", "", resourcePrefix);
}
public getFilters() {
return this.filters;
}
- public serialize() {
- return "[" + this.filters + "]";
- }
-
- private addCondition(field: string, cond: string, value?: string | string[], prefix: string = "", postfix: string = "") {
+ private addCondition(field: string, cond: string, value?: string | string[], prefix: string = "", postfix: string = "", resourcePrefix?: string) {
if (value) {
value = typeof value === "string"
? `"${prefix}${value}${postfix}"`
: `["${value.join(`","`)}"]`;
- const resourcePrefix = this.resourcePrefix
- ? _.snakeCase(this.resourcePrefix) + "."
+ const resPrefix = resourcePrefix
+ ? _.snakeCase(resourcePrefix) + "."
: "";
- this.filters += `${this.filters ? "," : ""}["${resourcePrefix}${_.snakeCase(field.toString())}","${cond}",${value}]`;
+ this.filters += `${this.filters ? "," : ""}["${resPrefix}${_.snakeCase(field)}","${cond}",${value}]`;
}
return this;
}
describe("OrderBuilder", () => {
it("should build correct order query", () => {
- const order = OrderBuilder
- .create()
+ const order = new OrderBuilder()
.addAsc("kind")
.addDesc("modifiedAt")
.getOrder();
expect(order).toEqual(["kind asc", "modified_at desc"]);
});
-
- it("should combine results with other builder", () => {
- const order = OrderBuilder
- .create()
- .addAsc("kind")
- .concat(OrderBuilder
- .create("properties")
- .addDesc("modifiedAt"))
- .getOrder();
- expect(order).toEqual(["kind asc", "properties.modified_at desc"]);
- });
});
// SPDX-License-Identifier: AGPL-3.0
import * as _ from "lodash";
-import { Resource } from "../../models/resource";
+import { Resource } from "~/models/resource";
-export class OrderBuilder<T extends Resource = Resource> {
-
- static create<T extends Resource = Resource>(prefix?: string){
- return new OrderBuilder<T>([], prefix);
- }
+export enum OrderDirection { ASC, DESC }
- private constructor(
- private order: string[] = [],
- private prefix = ""){}
+export class OrderBuilder<T extends Resource = Resource> {
- private addRule (direction: string, attribute: keyof T) {
- const prefix = this.prefix ? this.prefix + "." : "";
- const order = [...this.order, `${prefix}${_.snakeCase(attribute.toString())} ${direction}`];
- return new OrderBuilder<T>(order, prefix);
- }
+ constructor(private order: string[] = []) {}
- addAsc(attribute: keyof T) {
- return this.addRule("asc", attribute);
+ addOrder(direction: OrderDirection, attribute: keyof T, prefix?: string) {
+ this.order.push(`${prefix ? prefix + "." : ""}${_.snakeCase(attribute.toString())} ${direction === OrderDirection.ASC ? "asc" : "desc"}`);
+ return this;
}
- addDesc(attribute: keyof T) {
- return this.addRule("desc", attribute);
+ addAsc(attribute: keyof T, prefix?: string) {
+ return this.addOrder(OrderDirection.ASC, attribute, prefix);
}
- concat(orderBuilder: OrderBuilder){
- return new OrderBuilder<T>(
- this.order.concat(orderBuilder.getOrder()),
- this.prefix
- );
+ addDesc(attribute: keyof T, prefix?: string) {
+ return this.addOrder(OrderDirection.DESC, attribute, prefix);
}
getOrder() {
- return this.order.slice();
+ return this.order.join(",");
}
}
export const CONFIG_URL = process.env.REACT_APP_ARVADOS_CONFIG_URL || "/config.json";
export interface Config {
- API_HOST: string;
+ apiHost: string;
+ keepWebHost: string;
}
export const fetchConfig = () => {
return Axios
- .get<Config>(CONFIG_URL + "?nocache=" + (new Date()).getTime())
+ .get<ConfigJSON>(CONFIG_URL + "?nocache=" + (new Date()).getTime())
.then(response => response.data)
.catch(() => Promise.resolve(getDefaultConfig()))
.then(mapConfig);
};
-const mapConfig = (config: Config): Config => ({
- ...config,
- API_HOST: addProtocol(config.API_HOST)
+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 = (): Config => ({
- API_HOST: process.env.REACT_APP_ARVADOS_API_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}`;
//
// SPDX-License-Identifier: AGPL-3.0
-import { ResourceKind } from "../models/resource";
+import { ResourceKind } from "~/models/resource";
export const resourceLabel = (type: string) => {
switch (type) {
default:
return "Unknown";
}
-};
\ No newline at end of file
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { WebDAV } from "./webdav";
+
+describe('WebDAV', () => {
+ it('makes use of provided config', async () => {
+ const { open, load, setRequestHeader, createRequest } = mockCreateRequest();
+ const webdav = new WebDAV({ baseURL: 'http://foo.com/', headers: { Authorization: 'Basic' } }, createRequest);
+ const promise = webdav.propfind('foo');
+ load();
+ const request = await promise;
+ expect(open).toHaveBeenCalledWith('PROPFIND', 'http://foo.com/foo');
+ expect(setRequestHeader).toHaveBeenCalledWith('Authorization', 'Basic');
+ expect(request).toBeInstanceOf(XMLHttpRequest);
+ });
+
+ it('allows to modify defaults after instantiation', async () => {
+ const { open, load, setRequestHeader, createRequest } = mockCreateRequest();
+ const webdav = new WebDAV(undefined, createRequest);
+ webdav.defaults.baseURL = 'http://foo.com/';
+ webdav.defaults.headers = { Authorization: 'Basic' };
+ const promise = webdav.propfind('foo');
+ load();
+ const request = await promise;
+ expect(open).toHaveBeenCalledWith('PROPFIND', 'http://foo.com/foo');
+ expect(setRequestHeader).toHaveBeenCalledWith('Authorization', 'Basic');
+ expect(request).toBeInstanceOf(XMLHttpRequest);
+ });
+
+ it('PROPFIND', async () => {
+ const { open, load, createRequest } = mockCreateRequest();
+ const webdav = new WebDAV(undefined, createRequest);
+ const promise = webdav.propfind('foo');
+ load();
+ const request = await promise;
+ expect(open).toHaveBeenCalledWith('PROPFIND', 'foo');
+ expect(request).toBeInstanceOf(XMLHttpRequest);
+ });
+
+ 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 });
+ progress();
+ load();
+ const request = await promise;
+ expect(open).toHaveBeenCalledWith('PUT', 'foo');
+ expect(send).toHaveBeenCalledWith('Test data');
+ expect(onProgress).toHaveBeenCalled();
+ expect(request).toBeInstanceOf(XMLHttpRequest);
+ });
+
+ it('COPY', async () => {
+ const { open, setRequestHeader, load, createRequest } = mockCreateRequest();
+ const webdav = new WebDAV(undefined, createRequest);
+ const promise = webdav.copy('foo', 'foo-copy');
+ load();
+ const request = await promise;
+ expect(open).toHaveBeenCalledWith('COPY', 'foo');
+ expect(setRequestHeader).toHaveBeenCalledWith('Destination', 'foo-copy');
+ expect(request).toBeInstanceOf(XMLHttpRequest);
+ });
+
+ it('COPY - adds baseURL to Destination header', async () => {
+ const { open, setRequestHeader, load, createRequest } = mockCreateRequest();
+ const webdav = new WebDAV(undefined, createRequest);
+ webdav.defaults.baseURL = 'base/';
+ const promise = webdav.copy('foo', 'foo-copy');
+ load();
+ const request = await promise;
+ expect(open).toHaveBeenCalledWith('COPY', 'base/foo');
+ expect(setRequestHeader).toHaveBeenCalledWith('Destination', 'base/foo-copy');
+ expect(request).toBeInstanceOf(XMLHttpRequest);
+ });
+
+ it('MOVE', async () => {
+ const { open, setRequestHeader, load, createRequest } = mockCreateRequest();
+ const webdav = new WebDAV(undefined, createRequest);
+ const promise = webdav.move('foo', 'foo-copy');
+ load();
+ const request = await promise;
+ expect(open).toHaveBeenCalledWith('MOVE', 'foo');
+ expect(setRequestHeader).toHaveBeenCalledWith('Destination', 'foo-copy');
+ expect(request).toBeInstanceOf(XMLHttpRequest);
+ });
+
+ it('MOVE - adds baseURL to Destination header', async () => {
+ const { open, setRequestHeader, load, createRequest } = mockCreateRequest();
+ const webdav = new WebDAV(undefined, createRequest);
+ webdav.defaults.baseURL = 'base/';
+ const promise = webdav.move('foo', 'foo-moved');
+ load();
+ const request = await promise;
+ expect(open).toHaveBeenCalledWith('MOVE', 'base/foo');
+ expect(setRequestHeader).toHaveBeenCalledWith('Destination', 'base/foo-moved');
+ expect(request).toBeInstanceOf(XMLHttpRequest);
+ });
+
+ it('DELETE', async () => {
+ const { open, load, createRequest } = mockCreateRequest();
+ const webdav = new WebDAV(undefined, createRequest);
+ const promise = webdav.delete('foo');
+ load();
+ const request = await promise;
+ expect(open).toHaveBeenCalledWith('DELETE', 'foo');
+ expect(request).toBeInstanceOf(XMLHttpRequest);
+ });
+});
+
+const mockCreateRequest = () => {
+ const send = jest.fn();
+ const open = jest.fn();
+ const setRequestHeader = jest.fn();
+ const request = new XMLHttpRequest();
+ request.send = send;
+ request.open = open;
+ request.setRequestHeader = setRequestHeader;
+ const load = () => request.dispatchEvent(new Event('load'));
+ const progress = () => request.dispatchEvent(new Event('progress'));
+ return {
+ send,
+ open,
+ load,
+ progress,
+ setRequestHeader,
+ createRequest: () => request
+ };
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export class WebDAV {
+
+ defaults: WebDAVDefaults = {
+ baseURL: '',
+ headers: {},
+ };
+
+ constructor(config?: Partial<WebDAVDefaults>, private createRequest = () => new XMLHttpRequest()) {
+ if (config) {
+ this.defaults = { ...this.defaults, ...config };
+ }
+ }
+
+ propfind = (url: string, config: WebDAVRequestConfig = {}) =>
+ this.request({
+ ...config, url,
+ method: 'PROPFIND'
+ })
+
+ put = (url: string, data?: any, config: WebDAVRequestConfig = {}) =>
+ this.request({
+ ...config, url,
+ method: 'PUT',
+ data,
+ })
+
+ copy = (url: string, destination: string, config: WebDAVRequestConfig = {}) =>
+ this.request({
+ ...config, url,
+ method: 'COPY',
+ headers: { ...config.headers, Destination: this.defaults.baseURL + destination }
+ })
+
+ move = (url: string, destination: string, config: WebDAVRequestConfig = {}) =>
+ this.request({
+ ...config, url,
+ method: 'MOVE',
+ headers: { ...config.headers, Destination: this.defaults.baseURL + destination }
+ })
+
+ delete = (url: string, config: WebDAVRequestConfig = {}) =>
+ this.request({
+ ...config, url,
+ method: 'DELETE'
+ })
+
+ private request = (config: RequestConfig) => {
+ return new Promise<XMLHttpRequest>((resolve, reject) => {
+ const r = this.createRequest();
+ r.open(config.method, this.defaults.baseURL + config.url);
+
+ const headers = { ...this.defaults.headers, ...config.headers };
+ Object
+ .keys(headers)
+ .forEach(key => r.setRequestHeader(key, headers[key]));
+
+ if (config.onProgress) {
+ r.addEventListener('progress', config.onProgress);
+ }
+
+ r.addEventListener('load', () => resolve(r));
+ r.addEventListener('error', () => reject(r));
+
+ r.send(config.data);
+ });
+ }
+}
+export interface WebDAVRequestConfig {
+ headers?: {
+ [key: string]: string;
+ };
+ onProgress?: (event: ProgressEvent) => void;
+}
+
+interface WebDAVDefaults {
+ baseURL: string;
+ headers: { [key: string]: string };
+}
+
+interface RequestConfig {
+ method: string;
+ url: string;
+ headers?: { [key: string]: string };
+ data?: any;
+ onProgress?: (event: ProgressEvent) => void;
+}
import { TreeItem, TreeItemStatus } from '../tree/tree';
import { FileTreeData } from '../file-tree/file-tree-data';
import { FileTree } from '../file-tree/file-tree';
-import { IconButton, Grid, Typography, StyleRulesCallback, withStyles, WithStyles, CardHeader, CardContent, Card, Button } from '@material-ui/core';
+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";
+import { RootState } from "~/store/store";
+import { ServiceRepository } from "~/services/services";
export interface CollectionPanelFilesProps {
items: Array<TreeItem<FileTreeData>>;
import { Popover } from "../popover/popover";
import { IconButtonProps } from '@material-ui/core/IconButton';
import { DataColumns } from '../data-table/data-table';
-import { ArvadosTheme } from "../../common/custom-theme";
+import { ArvadosTheme } from "~/common/custom-theme";
interface ColumnSelectorDataProps {
columns: DataColumns<any>;
// SPDX-License-Identifier: AGPL-3.0
import * as React from "react";
-import { defaultTo, property } from 'lodash';
-import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, DialogContentText, CircularProgress } from "@material-ui/core";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, DialogContentText } from "@material-ui/core";
import { WithDialogProps } from "../../store/dialog/with-dialog";
-import { TextField } from "../text-field/text-field";
export interface ConfirmationDialogDataProps {
title: string;
// SPDX-License-Identifier: AGPL-3.0
import * as React from "react";
-import { mount, configure, shallow } from "enzyme";
+import { mount, configure } from "enzyme";
import * as Adapter from "enzyme-adapter-react-16";
import { ContextMenu } from "./context-menu";
import { ListItem } from "@material-ui/core";
import MoreVertIcon from "@material-ui/icons/MoreVert";
import { ColumnSelector } from "../column-selector/column-selector";
import { DataTable, DataColumns } from "../data-table/data-table";
-import { DataColumn } from "../data-table/data-column";
+import { DataColumn, SortDirection } from "../data-table/data-column";
import { DataTableFilterItem } from '../data-table-filters/data-table-filters';
import { SearchInput } from '../search-input/search-input';
-import { ArvadosTheme } from "../../common/custom-theme";
+import { ArvadosTheme } from "~/common/custom-theme";
import { DefaultView } from '../default-view/default-view';
import { IconType } from '../icon/icon';
export const DataExplorer = withStyles(styles)(
class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
render() {
- const {
- columns, onContextMenu, onFiltersChange, onSortToggle, extractKey,
- rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch,
- items, itemsAvailable, onRowClick, onRowDoubleClick, defaultIcon, defaultMessages, classes
+ const {
+ columns, onContextMenu, onFiltersChange, onSortToggle, extractKey,
+ rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch,
+ items, itemsAvailable, onRowClick, onRowDoubleClick, defaultIcon, defaultMessages, classes
} = this.props;
return <div>
{ items.length > 0 ? (
</Toolbar>
</Paper>
) : (
- <DefaultView
+ <DefaultView
classRoot={classes.defaultRoot}
icon={defaultIcon}
classIcon={classes.defaultIcon}
name: "Actions",
selected: true,
configurable: false,
+ sortDirection: SortDirection.NONE,
+ filters: [],
key: "context-actions",
render: this.renderContextMenuTrigger,
width: "auto"
import * as React from "react";
import { mount, configure } from "enzyme";
-import { DataTableFilters, DataTableFilterItem } from "./data-table-filters";
+import { DataTableFilters } from "./data-table-filters";
import * as Adapter from 'enzyme-adapter-react-16';
import { Checkbox, ButtonBase, ListItem, Button, ListItemText } from "@material-ui/core";
// SPDX-License-Identifier: AGPL-3.0
import { DataTableFilterItem } from "../data-table-filters/data-table-filters";
+import * as React from "react";
export interface DataColumn<T, F extends DataTableFilterItem = DataTableFilterItem> {
+ key?: React.Key;
name: string;
selected: boolean;
configurable: boolean;
- key?: React.Key;
- sortDirection?: SortDirection;
- filters?: F[];
+ sortDirection: SortDirection;
+ filters: F[];
render: (item: T) => React.ReactElement<any>;
renderHeader?: () => React.ReactElement<any>;
width?: string;
{
name: "Column 1",
render: () => <span />,
- selected: true
+ selected: true,
+ configurable: true
},
{
name: "Column 2",
render: () => <span />,
- selected: true
+ selected: true,
+ configurable: true
},
{
name: "Column 3",
render: () => <span />,
- selected: false
+ selected: false,
+ configurable: true
}
];
const dataTable = mount(<DataTable
{
name: "Column 1",
render: () => <span />,
- selected: true
+ selected: true,
+ configurable: true
}
];
const dataTable = mount(<DataTable
name: "Column 1",
renderHeader: () => <span>Column Header</span>,
render: () => <span />,
- selected: true
+ selected: true,
+ configurable: true
}
];
const dataTable = mount(<DataTable
name: "Column 1",
key: "column-1-key",
render: () => <span />,
- selected: true
+ selected: true,
+ configurable: true
}
];
const dataTable = mount(<DataTable
{
name: "Column 1",
render: (item) => <Typography>{item}</Typography>,
- selected: true
+ selected: true,
+ configurable: true
},
{
name: "Column 2",
render: (item) => <Button>{item}</Button>,
- selected: true
+ selected: true,
+ configurable: true
}
];
const dataTable = mount(<DataTable
name: "Column 1",
sortDirection: SortDirection.ASC,
selected: true,
+ configurable: true,
render: (item) => <Typography>{item}</Typography>
}];
const onSortToggle = jest.fn();
name: "Column 1",
sortDirection: SortDirection.ASC,
selected: true,
+ configurable: true,
filters: [{ name: "Filter 1", selected: true }],
render: (item) => <Typography>{item}</Typography>
}];
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { Table, TableBody, TableRow, TableCell, TableHead, TableSortLabel, StyleRulesCallback, Theme, WithStyles, withStyles, Typography } from '@material-ui/core';
+import { Table, TableBody, TableRow, TableCell, TableHead, TableSortLabel, StyleRulesCallback, Theme, WithStyles, withStyles } from '@material-ui/core';
import { DataColumn, SortDirection } from './data-column';
import { DataTableFilters, DataTableFilterItem } from "../data-table-filters/data-table-filters";
import * as React from 'react';
import Typography from '@material-ui/core/Typography';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import { ArvadosTheme } from '../../common/custom-theme';
+import { ArvadosTheme } from '~/common/custom-theme';
import * as classnames from "classnames";
type CssRules = 'attribute' | 'label' | 'value' | 'link';
it("opens on menu icon click", () => {
const dropdownMenu = shallow(<DropdownMenu id="test-menu" icon={<PaginationRightArrowIcon />} />);
dropdownMenu.find(IconButton).simulate("click", {currentTarget: {}});
- expect(dropdownMenu.state().anchorEl).toBeDefined();
+ expect((dropdownMenu.state() as any).anchorEl).toBeDefined();
});
it("closes on menu click", () => {
const dropdownMenu = shallow(<DropdownMenu id="test-menu" icon={<PaginationRightArrowIcon />} />);
dropdownMenu.find(Menu).simulate("click", {currentTarget: {}});
- expect(dropdownMenu.state().anchorEl).toBeUndefined();
+ expect((dropdownMenu.state() as any).anchorEl).toBeUndefined();
});
});
import { TreeItem } from "../tree/tree";
import { ProjectIcon, MoreOptionsIcon, DefaultIcon, CollectionIcon } from "../icon/icon";
import { Typography, IconButton, StyleRulesCallback, withStyles, WithStyles } from "@material-ui/core";
-import { formatFileSize } from "../../common/formatters";
+import { formatFileSize } from "~/common/formatters";
import { ListItemTextIcon } from "../list-item-text-icon/list-item-text-icon";
import { FileTreeData } from "./file-tree-data";
import { withStyles } from '@material-ui/core';
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 { formatFileSize, formatProgress, formatUploadSpeed } from "~/common/formatters";
+import { UploadFile } from "~/store/collections/uploader/collection-uploader-actions";
type CssRules = "root" | "dropzone" | "container" | "uploadIcon";
import * as React from 'react';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import { ArvadosTheme } from '../../common/custom-theme';
+import { ArvadosTheme } from '~/common/custom-theme';
import { ListItemIcon, ListItemText, Typography } from '@material-ui/core';
import { IconType } from '../icon/icon';
import * as classnames from "classnames";
it("opens on default trigger click", () => {
const popover = mount(<Popover />);
popover.find(DefaultTrigger).simulate("click");
- expect(popover.state().anchorEl).toBeDefined();
+ expect((popover.state() as any).anchorEl).toBeDefined();
});
it("renders custom trigger", () => {
it("opens on custom trigger click", () => {
const popover = mount(<Popover triggerComponent={CustomTrigger} />);
popover.find(CustomTrigger).simulate("click");
- expect(popover.state().anchorEl).toBeDefined();
+ expect((popover.state() as any).anchorEl).toBeDefined();
});
it("renders children when opened", () => {
);
popover.find(DefaultTrigger).simulate("click");
popover.find(CustomTrigger).simulate("click");
- expect(popover.state().anchorEl).toBeDefined();
+ expect((popover.state() as any).anchorEl).toBeDefined();
});
it("closes on content click if closeOnContentClick is set", () => {
const popover = mount(
);
popover.find(DefaultTrigger).simulate("click");
popover.find(CustomTrigger).simulate("click");
- expect(popover.state().anchorEl).toBeUndefined();
+ expect((popover.state() as any).anchorEl).toBeUndefined();
});
});
import * as React from "react";
import { InjectedFormProps, Field } from "redux-form";
-import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, DialogContentText, CircularProgress } from "@material-ui/core";
-import { WithDialogProps } from "../../store/dialog/with-dialog";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, DialogContentText, CircularProgress } from "@material-ui/core";
+import { WithDialogProps } from "~/store/dialog/with-dialog";
import { TextField } from "../text-field/text-field";
export const RenameDialog = (props: WithDialogProps<string> & InjectedFormProps<{ name: string }>) =>
import * as React from 'react';
import { ReactElement } from 'react';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import { ArvadosTheme } from '../../common/custom-theme';
+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 * as React from 'react';
import { WrappedFieldProps } from 'redux-form';
-import { ArvadosTheme } from '../../common/custom-theme';
+import { ArvadosTheme } from '~/common/custom-theme';
import { TextField as MaterialTextField, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
type CssRules = 'textField';
autoComplete='off'
fullWidth={true}
{...props.input}
- />);
\ No newline at end of file
+ />);
import CircularProgress from '@material-ui/core/CircularProgress';
import * as classnames from "classnames";
-import { ArvadosTheme } from '../../common/custom-theme';
+import { ArvadosTheme } from '~/common/custom-theme';
import { SidePanelRightArrowIcon } from '../icon/icon';
type CssRules = 'list'
fetchConfig()
.then(config => {
const history = createBrowserHistory();
- const services = createServices(config.API_HOST);
+ const services = createServices(config);
const store = configureStore(history, services);
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} {...props}/>;
//
// SPDX-License-Identifier: AGPL-3.0
-import { User } from "../../models/user";
+import { User } from "~/models/user";
import { AxiosInstance } from "axios";
export const API_TOKEN_KEY = 'apiToken';
import { CollectionService } from "../collection-service/collection-service";
import { parseKeepManifestText, stringifyKeepManifest } from "./collection-manifest-parser";
import { mapManifestToCollectionFilesTree } from "./collection-manifest-mapper";
-import { CommonResourceService } from "../../common/api/common-resource-service";
+import { CommonResourceService } from "~/common/api/common-resource-service";
import * as _ from "lodash";
export class CollectionFilesService {
// SPDX-License-Identifier: AGPL-3.0
import { uniqBy, groupBy } from 'lodash';
-import { KeepManifestStream, KeepManifestStreamFile, KeepManifest } from "../../models/keep-manifest";
-import { TreeNode, setNode, createTree, getNodeDescendants, getNodeValue } from '../../models/tree';
+import { KeepManifestStream, KeepManifestStreamFile, KeepManifest } from "~/models/keep-manifest";
+import { TreeNode, setNode, createTree, getNodeDescendants, getNodeValue } from '~/models/tree';
import { CollectionFilesTree, CollectionFile, CollectionDirectory, createCollectionDirectory, createCollectionFile, CollectionFileType } from '../../models/collection-file';
export const mapCollectionFilesTreeToManifest = (tree: CollectionFilesTree): KeepManifest => {
//
// SPDX-License-Identifier: AGPL-3.0
-import { KeepManifestStream, KeepManifestStreamFile, KeepManifest } from "../../models/keep-manifest";
+import { KeepManifestStream, KeepManifestStreamFile, KeepManifest } from "~/models/keep-manifest";
/**
* Documentation [http://doc.arvados.org/api/storage.html](http://doc.arvados.org/api/storage.html)
//
// SPDX-License-Identifier: AGPL-3.0
-import { CommonResourceService } from "../../common/api/common-resource-service";
-import { CollectionResource } from "../../models/collection";
+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 { FilterBuilder } from "../../common/api/filter-builder";
-import { CollectionFile, createCollectionFile } from "../../models/collection-file";
+import { FilterBuilder } from "~/common/api/filter-builder";
+import { CollectionFile, createCollectionFile } from "~/models/collection-file";
import { parseKeepManifestText, stringifyKeepManifest } from "../collection-files-service/collection-manifest-parser";
import * as _ from "lodash";
-import { KeepManifestStream } from "../../models/keep-manifest";
+import { KeepManifestStream } from "~/models/keep-manifest";
export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
}
uploadFiles(collectionUuid: string, files: File[], onProgress?: UploadProgress): Promise<CollectionResource | never> {
- const filters = FilterBuilder.create()
+ const filters = new FilterBuilder()
.addEqual("service_type", "proxy");
- return this.keepService.list({ filters }).then(data => {
+ return this.keepService.list({ filters: filters.getFilters() }).then(data => {
if (data.items && data.items.length > 0) {
const serviceHost =
(data.items[0].serviceSslFlag ? "https://" : "http://") +
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { LinkResource } from "../../models/link";
-import { GroupContentsResource, GroupContentsResourcePrefix } from "../groups-service/groups-service";
-import { OrderBuilder } from "../../common/api/order-builder";
-
-export class FavoriteOrderBuilder {
-
- static create(
- linkOrder = OrderBuilder.create<LinkResource>(),
- contentOrder = OrderBuilder.create<GroupContentsResource>()) {
- return new FavoriteOrderBuilder(linkOrder, contentOrder);
- }
-
- private constructor(
- private linkOrder: OrderBuilder<LinkResource>,
- private contentOrder: OrderBuilder<GroupContentsResource>
- ) { }
-
- addAsc(attribute: "name") {
- const linkOrder = this.linkOrder.addAsc(attribute);
- const contentOrder = this.contentOrder
- .concat(OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.COLLECTION).addAsc(attribute))
- .concat(OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROCESS).addAsc(attribute))
- .concat(OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROJECT).addAsc(attribute));
- return FavoriteOrderBuilder.create(linkOrder, contentOrder);
- }
-
- addDesc(attribute: "name") {
- const linkOrder = this.linkOrder.addDesc(attribute);
- const contentOrder = this.contentOrder
- .concat(OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.COLLECTION).addDesc(attribute))
- .concat(OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROCESS).addDesc(attribute))
- .concat(OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROJECT).addDesc(attribute));
- return FavoriteOrderBuilder.create(linkOrder, contentOrder);
- }
-
- getLinkOrder() {
- return this.linkOrder;
- }
-
- getContentOrder() {
- return this.contentOrder;
- }
-
-}
\ No newline at end of file
// SPDX-License-Identifier: AGPL-3.0
import { LinkService } from "../link-service/link-service";
-import { GroupsService, GroupContentsResource } from "../groups-service/groups-service";
+import { GroupsService } from "../groups-service/groups-service";
import { FavoriteService } from "./favorite-service";
-import { LinkClass, LinkResource } from "../../models/link";
-import { mockResourceService } from "../../common/api/common-resource-service.test";
-import { FilterBuilder } from "../../common/api/filter-builder";
+import { LinkClass } from "~/models/link";
+import { mockResourceService } from "~/common/api/common-resource-service.test";
+import { FilterBuilder } from "~/common/api/filter-builder";
describe("FavoriteService", () => {
it("unmarks resource as favorite", async () => {
const list = jest.fn().mockReturnValue(Promise.resolve({ items: [{ uuid: "linkUuid" }] }));
- const filters = FilterBuilder
- .create()
+ const filters = new FilterBuilder()
.addEqual('tailUuid', "userUuid")
.addEqual('headUuid', "resourceUuid")
.addEqual('linkClass', LinkClass.STAR);
it("lists favorite resources", async () => {
const list = jest.fn().mockReturnValue(Promise.resolve({ items: [{ headUuid: "headUuid" }] }));
- const listFilters = FilterBuilder
- .create()
+ const listFilters = new FilterBuilder()
.addEqual('tailUuid', "userUuid")
.addEqual('linkClass', LinkClass.STAR);
const contents = jest.fn().mockReturnValue(Promise.resolve({ items: [{ uuid: "resourceUuid" }] }));
- const contentFilters = FilterBuilder.create().addIn('uuid', ["headUuid"]);
+ const contentFilters = new FilterBuilder().addIn('uuid', ["headUuid"]);
linkService.list = list;
groupService.contents = contents;
const favoriteService = new FavoriteService(linkService, groupService);
it("checks if resources are present in favorites", async () => {
const list = jest.fn().mockReturnValue(Promise.resolve({ items: [{ headUuid: "foo" }] }));
- const listFilters = FilterBuilder
- .create()
+ const listFilters = new FilterBuilder()
.addIn("headUuid", ["foo", "oof"])
.addEqual("tailUuid", "userUuid")
.addEqual("linkClass", LinkClass.STAR);
import { LinkService } from "../link-service/link-service";
import { GroupsService, GroupContentsResource } from "../groups-service/groups-service";
-import { LinkResource, LinkClass } from "../../models/link";
-import { FilterBuilder } from "../../common/api/filter-builder";
-import { ListArguments, ListResults } from "../../common/api/common-resource-service";
-import { FavoriteOrderBuilder } from "./favorite-order-builder";
-import { OrderBuilder } from "../../common/api/order-builder";
+import { LinkClass } from "~/models/link";
+import { FilterBuilder, joinFilters } from "~/common/api/filter-builder";
+import { ListResults } from "~/common/api/common-resource-service";
export interface FavoriteListArguments {
limit?: number;
offset?: number;
- filters?: FilterBuilder;
- order?: FavoriteOrderBuilder;
+ filters?: string;
+ linkOrder?: string;
+ contentOrder?: string;
}
export class FavoriteService {
constructor(
private linkService: LinkService,
private groupsService: GroupsService
- ) { }
+ ) {}
create(data: { userUuid: string; resource: { uuid: string; name: string } }) {
return this.linkService.create({
delete(data: { userUuid: string; resourceUuid: string; }) {
return this.linkService
.list({
- filters: FilterBuilder
- .create()
+ filters: new FilterBuilder()
.addEqual('tailUuid', data.userUuid)
.addEqual('headUuid', data.resourceUuid)
.addEqual('linkClass', LinkClass.STAR)
+ .getFilters()
})
.then(results => Promise.all(
results.items.map(item => this.linkService.delete(item.uuid))));
}
- list(userUuid: string, { filters, limit, offset, order }: FavoriteListArguments = {}): Promise<ListResults<GroupContentsResource>> {
- const listFilter = FilterBuilder
- .create()
+ list(userUuid: string, { filters, limit, offset, linkOrder, contentOrder }: FavoriteListArguments = {}): Promise<ListResults<GroupContentsResource>> {
+ const listFilters = new FilterBuilder()
.addEqual('tailUuid', userUuid)
- .addEqual('linkClass', LinkClass.STAR);
+ .addEqual('linkClass', LinkClass.STAR)
+ .getFilters();
return this.linkService
.list({
- filters: filters ? filters.concat(listFilter) : listFilter,
+ filters: joinFilters(filters, listFilters),
limit,
offset,
- order: order ? order.getLinkOrder() : OrderBuilder.create<LinkResource>()
+ order: linkOrder
})
.then(results => {
const uuids = results.items.map(item => item.headUuid);
return this.groupsService.contents(userUuid, {
limit,
offset,
- order: order ? order.getContentOrder() : OrderBuilder.create<GroupContentsResource>(),
- filters: FilterBuilder.create().addIn('uuid', uuids),
+ order: contentOrder,
+ filters: new FilterBuilder().addIn('uuid', uuids).getFilters(),
recursive: true
});
});
checkPresenceInFavorites(userUuid: string, resourceUuids: string[]): Promise<Record<string, boolean>> {
return this.linkService
.list({
- filters: FilterBuilder
- .create()
+ filters: new FilterBuilder()
.addIn("headUuid", resourceUuids)
.addEqual("tailUuid", userUuid)
.addEqual("linkClass", LinkClass.STAR)
+ .getFilters()
})
.then(({ items }) => resourceUuids.reduce((results, uuid) => {
const isFavorite = items.some(item => item.headUuid === uuid);
// SPDX-License-Identifier: AGPL-3.0
import * as _ from "lodash";
-import { CommonResourceService, ListResults } from "../../common/api/common-resource-service";
-import { FilterBuilder } from "../../common/api/filter-builder";
-import { OrderBuilder } from "../../common/api/order-builder";
+import { CommonResourceService, ListResults } from "~/common/api/common-resource-service";
import { AxiosInstance } from "axios";
-import { GroupResource } from "../../models/group";
-import { CollectionResource } from "../../models/collection";
-import { ProjectResource } from "../../models/project";
-import { ProcessResource } from "../../models/process";
+import { GroupResource } from "~/models/group";
+import { CollectionResource } from "~/models/collection";
+import { ProjectResource } from "~/models/project";
+import { ProcessResource } from "~/models/process";
export interface ContentsArguments {
limit?: number;
offset?: number;
- order?: OrderBuilder;
- filters?: FilterBuilder;
+ order?: string;
+ filters?: string;
recursive?: boolean;
}
const { filters, order, ...other } = args;
const params = {
...other,
- filters: filters ? filters.serialize() : undefined,
- order: order ? order.getOrder() : undefined
+ filters: filters ? `[${filters}]` : undefined,
+ order: order ? order : undefined
};
return this.serverApi
.get(this.resourceType + `${uuid}/contents/`, {
//\r
// SPDX-License-Identifier: AGPL-3.0\r
\r
-import { CommonResourceService } from "../../common/api/common-resource-service";\r
+import { CommonResourceService } from "~/common/api/common-resource-service";\r
import { AxiosInstance } from "axios";\r
-import { KeepResource } from "../../models/keep";\r
+import { KeepResource } from "~/models/keep";\r
\r
export class KeepService extends CommonResourceService<KeepResource> {\r
constructor(serverApi: AxiosInstance) {\r
//
// SPDX-License-Identifier: AGPL-3.0
-import { CommonResourceService } from "../../common/api/common-resource-service";
-import { LinkResource } from "../../models/link";
+import { CommonResourceService } from "~/common/api/common-resource-service";
+import { LinkResource } from "~/models/link";
import { AxiosInstance } from "axios";
export class LinkService extends CommonResourceService<LinkResource> {
constructor(serverApi: AxiosInstance) {
super(serverApi, "links");
}
-}
\ No newline at end of file
+}
import axios from "axios";
import { ProjectService } from "./project-service";
-import { FilterBuilder } from "../../common/api/filter-builder";
-import { ProjectResource } from "../../models/project";
+import { FilterBuilder } from "~/common/api/filter-builder";
describe("CommonResourceService", () => {
const axiosInstance = axios.create();
});
});
-
it("#list has groupClass filter set by default", async () => {
axiosInstance.get = jest.fn(() => Promise.resolve({ data: {} }));
const projectService = new ProjectService(axiosInstance);
const resource = await projectService.list();
expect(axiosInstance.get).toHaveBeenCalledWith("/groups/", {
params: {
- filters: FilterBuilder
- .create()
+ filters: new FilterBuilder()
.addEqual("groupClass", "project")
- .serialize()
+ .getFilters()
}
});
});
-
});
//
// SPDX-License-Identifier: AGPL-3.0
-import { GroupsService, ContentsArguments } from "../groups-service/groups-service";
-import { ProjectResource } from "../../models/project";
-import { GroupClass } from "../../models/group";
-import { ListArguments } from "../../common/api/common-resource-service";
-import { FilterBuilder } from "../../common/api/filter-builder";
+import { GroupsService } from "../groups-service/groups-service";
+import { ProjectResource } from "~/models/project";
+import { GroupClass } from "~/models/group";
+import { ListArguments } from "~/common/api/common-resource-service";
+import { FilterBuilder, joinFilters } from "~/common/api/filter-builder";
export class ProjectService extends GroupsService<ProjectResource> {
list(args: ListArguments = {}) {
return super.list({
...args,
- filters: this.addProjectFilter(args.filters)
+ filters: joinFilters(
+ args.filters,
+ new FilterBuilder()
+ .addEqual("groupClass", GroupClass.PROJECT)
+ .getFilters()
+ )
});
}
-
- private addProjectFilter(filters?: FilterBuilder) {
- return FilterBuilder
- .create()
- .concat(filters
- ? filters
- : FilterBuilder.create())
- .concat(FilterBuilder
- .create()
- .addEqual("groupClass", GroupClass.PROJECT));
- }
}
//
// SPDX-License-Identifier: AGPL-3.0
+import Axios, { AxiosInstance } from "axios";
import { AuthService } from "./auth-service/auth-service";
import { GroupsService } from "./groups-service/groups-service";
import { ProjectService } from "./project-service/project-service";
import { LinkService } from "./link-service/link-service";
import { FavoriteService } from "./favorite-service/favorite-service";
-import { AxiosInstance } from "axios";
import { CollectionService } from "./collection-service/collection-service";
import { TagService } from "./tag-service/tag-service";
-import Axios from "axios";
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";
-export interface ServiceRepository {
- apiClient: AxiosInstance;
+export type ServiceRepository = ReturnType<typeof createServices>;
- authService: AuthService;
- keepService: KeepService;
- groupsService: GroupsService;
- projectService: ProjectService;
- linkService: LinkService;
- favoriteService: FavoriteService;
- tagService: TagService;
- collectionService: CollectionService;
- collectionFilesService: CollectionFilesService;
-}
-
-export const createServices = (baseUrl: string): ServiceRepository => {
+export const createServices = (config: Config) => {
const apiClient = Axios.create();
- apiClient.defaults.baseURL = `${baseUrl}/arvados/v1`;
+ apiClient.defaults.baseURL = `${config.apiHost}/arvados/v1`;
+
+ const webdavClient = new WebDAV();
+ webdavClient.defaults.baseURL = config.keepWebHost;
- const authService = new AuthService(apiClient, baseUrl);
+ const authService = new AuthService(apiClient, config.apiHost);
const keepService = new KeepService(apiClient);
const groupsService = new GroupsService(apiClient);
const projectService = new ProjectService(apiClient);
return {
apiClient,
+ webdavClient,
authService,
keepService,
groupsService,
collectionFilesService
};
};
+
// SPDX-License-Identifier: AGPL-3.0
import { LinkService } from "../link-service/link-service";
-import { LinkClass } from "../../models/link";
-import { FilterBuilder } from "../../common/api/filter-builder";
-import { TagTailType, TagResource } from "../../models/tag";
-import { OrderBuilder } from "../../common/api/order-builder";
+import { LinkClass } from "~/models/link";
+import { FilterBuilder } from "~/common/api/filter-builder";
+import { TagTailType, TagResource } from "~/models/tag";
+import { OrderBuilder } from "~/common/api/order-builder";
export class TagService {
}
list(uuid: string) {
- const filters = FilterBuilder
- .create()
+ const filters = new FilterBuilder()
.addEqual("headUuid", uuid)
.addEqual("tailUuid", TagTailType.COLLECTION)
- .addEqual("linkClass", LinkClass.TAG);
+ .addEqual("linkClass", LinkClass.TAG)
+ .getFilters();
- const order = OrderBuilder
- .create<TagResource>()
- .addAsc('createdAt');
+ const order = new OrderBuilder<TagResource>()
+ .addAsc('createdAt')
+ .getOrder();
return this.linkService
.list({ filters, order })
return results.items.map((tag => tag as TagResource ));
});
}
-
}
import { ofType, default as unionize, UnionOf } from "unionize";
import { Dispatch } from "redux";
-import { User } from "../../models/user";
+import { User } from "~/models/user";
import { RootState } from "../store";
-import { ServiceRepository } from "../../services/services";
+import { ServiceRepository } from "~/services/services";
import { AxiosInstance } from "axios";
export const authActions = unionize({
USER_DETAILS_REQUEST: {},
USER_DETAILS_SUCCESS: ofType<User>()
}, {
- tag: 'type',
- value: 'payload'
-});
+ tag: 'type',
+ value: 'payload'
+ });
-function setAuthorizationHeader(client: AxiosInstance, token: string) {
- client.defaults.headers.common = {
+function setAuthorizationHeader(services: ServiceRepository, token: string) {
+ services.apiClient.defaults.headers.common = {
+ Authorization: `OAuth2 ${token}`
+ };
+ services.webdavClient.defaults.headers = {
Authorization: `OAuth2 ${token}`
};
}
const user = services.authService.getUser();
const token = services.authService.getApiToken();
if (token) {
- setAuthorizationHeader(services.apiClient, token);
+ setAuthorizationHeader(services, token);
}
if (token && user) {
dispatch(authActions.INIT({ user, token }));
export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
services.authService.saveApiToken(token);
- setAuthorizationHeader(services.apiClient, token);
+ setAuthorizationHeader(services, token);
dispatch(authActions.SAVE_API_TOKEN(token));
};
USER_LAST_NAME_KEY,
USER_OWNER_UUID_KEY,
USER_UUID_KEY
-} from "../../services/auth-service/auth-service";
+} from "~/services/auth-service/auth-service";
import 'jest-localstorage-mock';
-import { createServices } from "../../services/services";
+import { createServices } from "~/services/services";
import { configureStore, RootStore } from "../store";
import createBrowserHistory from "history/createBrowserHistory";
let store: RootStore;
beforeEach(() => {
- store = configureStore(createBrowserHistory(), createServices("/arvados/v1"));
+ store = configureStore(createBrowserHistory(), createServices({ apiHost: "/arvados/v1", keepWebHost: "" }));
localStorage.clear();
- reducer = authReducer(createServices("/arvados/v1"));
+ reducer = authReducer(createServices({ apiHost: "/arvados/v1", keepWebHost: "" }));
});
it('should initialise state with user and api token from local storage', () => {
import { AuthAction, authActions } from "./auth-action";
import 'jest-localstorage-mock';
-import { createServices } from "../../services/services";
+import { createServices } from "~/services/services";
describe('auth-reducer', () => {
let reducer: (state: AuthState | undefined, action: AuthAction) => any;
beforeAll(() => {
localStorage.clear();
- reducer = authReducer(createServices("/arvados/v1"));
+ reducer = authReducer(createServices({ apiHost: "/arvados/v1", keepWebHost: "" }));
});
it('should correctly initialise state', () => {
uuid: "uuid",
ownerUuid: "ownerUuid"
};
- const state = reducer(initialState, authActions.INIT({user, token: "token"}));
+ const state = reducer(initialState, authActions.INIT({ user, token: "token" }));
expect(state).toEqual({
apiToken: "token",
user
// SPDX-License-Identifier: AGPL-3.0
import { authActions, AuthAction } from "./auth-action";
-import { User } from "../../models/user";
-import { ServiceRepository } from "../../services/services";
+import { User } from "~/models/user";
+import { ServiceRepository } from "~/services/services";
export interface AuthState {
user?: User;
import { unionize, ofType, UnionOf } from "unionize";
import { Dispatch } from "redux";
-import { ResourceKind } from "../../models/resource";
-import { CollectionResource } from "../../models/collection";
+import { ResourceKind } from "~/models/resource";
+import { CollectionResource } from "~/models/collection";
import { collectionPanelFilesAction } from "./collection-panel-files/collection-panel-files-actions";
-import { createTree } from "../../models/tree";
+import { createTree } from "~/models/tree";
import { RootState } from "../store";
-import { ServiceRepository } from "../../services/services";
-import { TagResource, TagProperty } from "../../models/tag";
+import { ServiceRepository } from "~/services/services";
+import { TagResource, TagProperty } from "~/models/tag";
import { snackbarActions } from "../snackbar/snackbar-actions";
export const collectionPanelActions = unionize({
});
};
-export const loadCollectionTags = (uuid: string) =>
+export const loadCollectionTags = (uuid: string) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(collectionPanelActions.LOAD_COLLECTION_TAGS({ uuid }));
return services.tagService
};
-export const createCollectionTag = (data: TagProperty) =>
+export const createCollectionTag = (data: TagProperty) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(collectionPanelActions.CREATE_COLLECTION_TAG({ data }));
const item = getState().collectionPanel.item;
});
};
-export const deleteCollectionTag = (uuid: string) =>
+export const deleteCollectionTag = (uuid: string) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(collectionPanelActions.DELETE_COLLECTION_TAG({ uuid }));
return services.linkService
hideDuration: 2000
}));
});
- };
\ No newline at end of file
+ };
// SPDX-License-Identifier: AGPL-3.0
import { default as unionize, ofType, UnionOf } from "unionize";
-import { CollectionFilesTree } from "../../../models/collection-file";
+import { CollectionFilesTree } from "~/models/collection-file";
export const collectionPanelFilesAction = unionize({
SET_COLLECTION_FILES: ofType<CollectionFilesTree>(),
UNSELECT_ALL_COLLECTION_FILES: ofType<{}>(),
}, { tag: 'type', value: 'payload' });
-export type CollectionPanelFilesAction = UnionOf<typeof collectionPanelFilesAction>;
\ No newline at end of file
+export type CollectionPanelFilesAction = UnionOf<typeof collectionPanelFilesAction>;
import { collectionPanelFilesReducer } from "./collection-panel-files-reducer";
import { collectionPanelFilesAction } from "./collection-panel-files-actions";
-import { CollectionFile, CollectionDirectory, createCollectionFile, createCollectionDirectory } from "../../../models/collection-file";
-import { createTree, setNode, getNodeValue, mapTreeValues, Tree } from "../../../models/tree";
+import { CollectionFile, CollectionDirectory, createCollectionFile, createCollectionDirectory } from "~/models/collection-file";
+import { createTree, setNode, getNodeValue, mapTreeValues } from "~/models/tree";
import { CollectionPanelFile, CollectionPanelDirectory } from "./collection-panel-files-state";
describe('CollectionPanelFilesReducer', () => {
import { CollectionPanelFilesState, CollectionPanelFile, CollectionPanelDirectory, mapCollectionFileToCollectionPanelFile } 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 { CollectionFileType } from "../../../models/collection-file";
+import { createTree, mapTreeValues, getNode, setNode, getNodeAncestors, getNodeDescendants, setNodeValueWith, mapTree } from "~/models/tree";
+import { CollectionFileType } from "~/models/collection-file";
export const collectionPanelFilesReducer = (state: CollectionPanelFilesState = createTree(), action: CollectionPanelFilesAction) => {
return collectionPanelFilesAction.match(action, {
//
// SPDX-License-Identifier: AGPL-3.0
-import { CollectionFile, CollectionDirectory, CollectionFileType } from '../../../models/collection-file';
-import { Tree, TreeNode } from '../../../models/tree';
+import { CollectionFile, CollectionDirectory, CollectionFileType } from '~/models/collection-file';
+import { Tree, TreeNode } from '~/models/tree';
export type CollectionPanelFilesState = Tree<CollectionPanelDirectory | CollectionPanelFile>;
? { ...node.value, selected: false, collapsed: true }
: { ...node.value, selected: false }
};
-};
\ No newline at end of file
+};
// SPDX-License-Identifier: AGPL-3.0
import { collectionPanelActions, CollectionPanelAction } from "./collection-panel-action";
-import { CollectionResource } from "../../models/collection";
-import { TagResource } from "../../models/tag";
+import { CollectionResource } from "~/models/collection";
+import { TagResource } from "~/models/tag";
export interface CollectionPanelState {
item: CollectionResource | null;
import { Dispatch } from "redux";
import { RootState } from "../../store";
-import { CollectionResource } from '../../../models/collection';
-import { ServiceRepository } from "../../../services/services";
+import { CollectionResource } from '~/models/collection';
+import { ServiceRepository } from "~/services/services";
import { collectionUploaderActions } from "../uploader/collection-uploader-actions";
import { reset } from "redux-form";
import { Dispatch } from "redux";
import { RootState } from "../../store";
-import { ServiceRepository } from "../../../services/services";
-import { CollectionResource } from '../../../models/collection';
+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 }>(),
.then(collection => {
dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: collection as CollectionResource }));
dispatch(collectionUpdaterActions.UPDATE_COLLECTION_SUCCESS());
+ dispatch<any>(updateDetails(collection));
}
);
};
// SPDX-License-Identifier: AGPL-3.0
import { default as unionize, ofType, UnionOf } from "unionize";
-import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
-import { DataColumns } from "../../components/data-table/data-table";
+import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters";
+import { DataColumns } from "~/components/data-table/data-table";
export const dataExplorerActions = unionize({
RESET_PAGINATION: ofType<{ id: string }>(),
import { Dispatch, MiddlewareAPI } from "redux";
import { RootState } from "../store";
+import { DataColumns } from "~/components/data-table/data-table";
+import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters";
export abstract class DataExplorerMiddlewareService {
protected readonly id: string;
return this.id;
}
+ public getColumnFilters<T, F extends DataTableFilterItem>(columns: DataColumns<T, F>, columnName: string): F[] {
+ const column = columns.find(c => c.name === columnName);
+ return column ? column.filters.filter(f => f.selected) : [];
+ }
+
abstract requestItems(api: MiddlewareAPI<Dispatch, RootState>): void;
}
import { DataExplorerMiddlewareService } from "./data-explorer-middleware-service";
import { dataExplorerMiddleware } from "./data-explorer-middleware";
import { MiddlewareAPI } from "redux";
-import { DataColumns } from "../../components/data-table/data-table";
+import { DataColumns } from "~/components/data-table/data-table";
import { dataExplorerActions } from "./data-explorer-action";
+import { SortDirection } from "~/components/data-table/data-column";
describe("DataExplorerMiddleware", () => {
-
+
it("handles only actions that are identified by service id", () => {
const config = {
id: "ServiceId",
name: "Column",
selected: true,
configurable: false,
+ sortDirection: SortDirection.NONE,
+ filters: [],
render: jest.fn()
}],
requestItems: jest.fn(),
name: "Column",
selected: true,
configurable: false,
+ sortDirection: SortDirection.NONE,
+ filters: [],
render: jest.fn()
}],
requestItems: jest.fn(),
const columns: DataColumns<any> = [{
name: "Column 1",
render: jest.fn(),
- selected: true
+ selected: true,
+ configurable: true
}];
const state = dataExplorerReducer(undefined,
dataExplorerActions.SET_COLUMNS({ id: "Data explorer", columns }));
name: "Column 1",
render: jest.fn(),
selected: true,
+ configurable: true,
sortDirection: SortDirection.ASC
}, {
name: "Column 2",
render: jest.fn(),
selected: true,
+ configurable: true,
sortDirection: SortDirection.NONE,
}];
const state = dataExplorerReducer({ "Data explorer": { ...initialDataExplorer, columns } },
name: "Column 1",
render: jest.fn(),
selected: true,
+ configurable: true
}];
const filters: DataTableFilterItem[] = [{
});
it('should set items', () => {
- const state = dataExplorerReducer({ "Data explorer": undefined },
+ const state = dataExplorerReducer({},
dataExplorerActions.SET_ITEMS({
id: "Data explorer",
items: ["Item 1", "Item 2"],
});
it('should set page', () => {
- const state = dataExplorerReducer({ "Data explorer": undefined },
+ const state = dataExplorerReducer({},
dataExplorerActions.SET_PAGE({ id: "Data explorer", page: 2 }));
expect(state["Data explorer"].page).toEqual(2);
});
it('should set rows per page', () => {
- const state = dataExplorerReducer({ "Data explorer": undefined },
+ const state = dataExplorerReducer({},
dataExplorerActions.SET_ROWS_PER_PAGE({ id: "Data explorer", rowsPerPage: 5 }));
expect(state["Data explorer"].rowsPerPage).toEqual(5);
});
//
// SPDX-License-Identifier: AGPL-3.0
-import { DataColumn, toggleSortDirection, resetSortDirection } from "../../components/data-table/data-column";
+import { DataColumn, toggleSortDirection, resetSortDirection } from "~/components/data-table/data-column";
import { dataExplorerActions, DataExplorerAction } from "./data-explorer-action";
-import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
-import { DataColumns } from "../../components/data-table/data-table";
+import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters";
+import { DataColumns } from "~/components/data-table/data-table";
export interface DataExplorer {
columns: DataColumns<any>;
import { unionize, ofType, UnionOf } from "unionize";
import { Dispatch } from "redux";
-import { Resource, ResourceKind } from "../../models/resource";
+import { Resource, ResourceKind } from "~/models/resource";
import { RootState } from "../store";
-import { ServiceRepository } from "../../services/services";
+import { ServiceRepository } from "~/services/services";
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' });
export type DetailsPanelAction = UnionOf<typeof detailsPanelActions>;
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:
// SPDX-License-Identifier: AGPL-3.0
import { detailsPanelActions, DetailsPanelAction } from "./details-panel-action";
-import { Resource } from "../../models/resource";
+import { Resource } from "~/models/resource";
export interface DetailsPanelState {
item: Resource | null;
export const detailsPanelReducer = (state: DetailsPanelState = initialState, action: DetailsPanelAction) =>
detailsPanelActions.match(action, {
default: () => state,
- LOAD_DETAILS: () => state,
LOAD_DETAILS_SUCCESS: ({ item }) => ({ ...state, item }),
TOGGLE_DETAILS_PANEL: () => ({ ...state, isOpened: !state.isOpened })
});
// SPDX-License-Identifier: AGPL-3.0
import { DataExplorerMiddlewareService } from "../data-explorer/data-explorer-middleware-service";
-import { FavoritePanelFilter, FavoritePanelColumnNames } from "../../views/favorite-panel/favorite-panel";
+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 { FavoriteOrderBuilder } from "../../services/favorite-service/favorite-order-builder";
-import { ServiceRepository } from "../../services/services";
-import { SortDirection } from "../../components/data-table/data-column";
-import { FilterBuilder } from "../../common/api/filter-builder";
-import { LinkResource } from "../../models/link";
+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 { 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";
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(
- ({ sortDirection }) => sortDirection !== undefined && sortDirection !== "none"
- );
- const typeFilters = getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
- const order = FavoriteOrderBuilder.create();
- if (typeFilters.length > 0) {
- this.services.favoriteService
- .list(this.services.authService.getUuid()!, {
- limit: dataExplorer.rowsPerPage,
- offset: dataExplorer.page * dataExplorer.rowsPerPage,
- order: sortColumn!.name === FavoritePanelColumnNames.NAME
- ? sortColumn!.sortDirection === SortDirection.ASC
- ? order.addDesc("name")
- : order.addAsc("name")
- : order,
- filters: FilterBuilder
- .create()
- .addIsA("headUuid", typeFilters.map(filter => filter.type))
- .addILike("name", dataExplorer.searchValue)
- })
- .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)));
- });
- } else {
- api.dispatch(favoritePanelActions.SET_ITEMS({
- items: [],
- itemsAvailable: 0,
- page: 0,
- rowsPerPage: dataExplorer.rowsPerPage
- }));
+ const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+ const typeFilters = this.getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
+
+ const linkOrder = new OrderBuilder<LinkResource>();
+ const contentOrder = new OrderBuilder<GroupContentsResource>();
+
+ 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
+ }));
+ });
}
}
-
-const getColumnFilters = (columns: DataColumns<FavoritePanelItem, FavoritePanelFilter>, columnName: string) => {
- const column = columns.find(c => c.name === columnName);
- return column && column.filters ? column.filters.filter(f => f.selected) : [];
-};
import { RootState } from "../store";
import { checkFavorite } from "./favorites-reducer";
import { snackbarActions } from "../snackbar/snackbar-actions";
-import { ServiceRepository } from "../../services/services";
+import { ServiceRepository } from "~/services/services";
export const favoritesActions = unionize({
TOGGLE_FAVORITE: ofType<{ resourceUuid: string }>(),
import { Dispatch } from "redux";
import { getProjectList, projectActions } from "../project/project-action";
import { push } from "react-router-redux";
-import { TreeItemStatus } from "../../components/tree/tree";
+import { TreeItemStatus } from "~/components/tree/tree";
import { findTreeItem } from "../project/project-reducer";
import { RootState } from "../store";
-import { ResourceKind } from "../../models/resource";
+import { Resource, ResourceKind } from "~/models/resource";
import { projectPanelActions } from "../project-panel/project-panel-action";
-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 { 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 { SidePanelIdentifiers } from "../side-panel/side-panel-reducer";
-import { getUuidObjectType, ObjectTypes } from "../../models/object-types";
+import { getUuidObjectType, ObjectTypes } from "~/models/object-types";
export const getResourceUrl = (resourceKind: ResourceKind, resourceUuid: string): string => {
switch (resourceKind) {
dispatch(projectPanelActions.RESET_PAGINATION());
dispatch(projectPanelActions.REQUEST_ITEMS());
}));
-
} else {
const uuid = services.authService.getUuid();
if (itemId === uuid) {
// SPDX-License-Identifier: AGPL-3.0
import { DataExplorerMiddlewareService } from "../data-explorer/data-explorer-middleware-service";
-import { ProjectPanelColumnNames, ProjectPanelFilter } from "../../views/project-panel/project-panel";
+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 } from "../../common/api/order-builder";
-import { FilterBuilder } from "../../common/api/filter-builder";
-import { ProcessResource } from "../../models/process";
-import { GroupContentsResourcePrefix, GroupContentsResource } from "../../services/groups-service/groups-service";
+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 { Dispatch, MiddlewareAPI } from "redux";
+import { ProjectResource } from "~/models/project";
export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
const state = api.getState();
const dataExplorer = state.dataExplorer[this.getId()];
const columns = dataExplorer.columns as DataColumns<ProjectPanelItem, ProjectPanelFilter>;
- const typeFilters = getColumnFilters(columns, ProjectPanelColumnNames.TYPE);
- const statusFilters = getColumnFilters(columns, ProjectPanelColumnNames.STATUS);
- const sortColumn = dataExplorer.columns.find(({ sortDirection }) => Boolean(sortDirection && sortDirection !== "none"));
- const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC ? SortDirection.ASC : SortDirection.DESC;
- if (typeFilters.length > 0) {
- this.services.groupsService
- .contents(state.projects.currentItemId, {
- limit: dataExplorer.rowsPerPage,
- offset: dataExplorer.page * dataExplorer.rowsPerPage,
- order: sortColumn
- ? sortColumn.name === ProjectPanelColumnNames.NAME
- ? getOrder("name", sortDirection)
- : getOrder("createdAt", sortDirection)
- : OrderBuilder.create(),
- filters: FilterBuilder
- .create()
- .concat(FilterBuilder
- .create()
- .addIsA("uuid", typeFilters.map(f => f.type)))
- .concat(FilterBuilder
- .create(GroupContentsResourcePrefix.PROCESS)
- .addIn("state", statusFilters.map(f => f.type)))
- .concat(getSearchFilter(dataExplorer.searchValue))
- })
- .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)));
- });
- } else {
- api.dispatch(projectPanelActions.SET_ITEMS({
- items: [],
- itemsAvailable: 0,
- page: 0,
- rowsPerPage: dataExplorer.rowsPerPage
- }));
- }
- }
-}
+ 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 getColumnFilters = (columns: DataColumns<ProjectPanelItem, ProjectPanelFilter>, columnName: string) => {
- const column = columns.find(c => c.name === columnName);
- return column && column.filters ? column.filters.filter(f => f.selected) : [];
-};
+ const order = new OrderBuilder<ProjectResource>();
-const getOrder = (attribute: "name" | "createdAt", direction: SortDirection) =>
- [
- OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.COLLECTION),
- OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROCESS),
- OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROJECT)
- ].reduce((acc, b) =>
- acc.concat(direction === SortDirection.ASC
- ? b.addAsc(attribute)
- : b.addDesc(attribute)), OrderBuilder.create());
+ if (sortColumn) {
+ const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
+ ? OrderDirection.ASC
+ : OrderDirection.DESC;
-const getSearchFilter = (searchValue: string) =>
- searchValue
- ? [
- FilterBuilder.create(GroupContentsResourcePrefix.COLLECTION),
- FilterBuilder.create(GroupContentsResourcePrefix.PROCESS),
- FilterBuilder.create(GroupContentsResourcePrefix.PROJECT)]
- .reduce((acc, b) =>
- acc.concat(b.addILike("name", searchValue)), FilterBuilder.create())
- : FilterBuilder.create();
+ 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);
+ }
+
+ 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
+ }));
+ });
+ }
+}
// SPDX-License-Identifier: AGPL-3.0
import { default as unionize, ofType, UnionOf } from "unionize";
-import { ProjectResource } from "../../models/project";
+import { ProjectResource } from "~/models/project";
import { Dispatch } from "redux";
-import { FilterBuilder } from "../../common/api/filter-builder";
+import { FilterBuilder } from "~/common/api/filter-builder";
import { RootState } from "../store";
import { checkPresenceInFavorites } from "../favorites/favorites-actions";
-import { ServiceRepository } from "../../services/services";
+import { ServiceRepository } from "~/services/services";
+import { projectPanelActions } from "~/store/project-panel/project-panel-action";
+import { updateDetails } from "~/store/details-panel/details-panel-action";
export const projectActions = unionize({
OPEN_PROJECT_CREATOR: ofType<{ ownerUuid: string }>(),
CLOSE_PROJECT_CREATOR: ofType<{}>(),
CREATE_PROJECT: ofType<Partial<ProjectResource>>(),
CREATE_PROJECT_SUCCESS: ofType<ProjectResource>(),
+ OPEN_PROJECT_UPDATER: ofType<{ uuid: string}>(),
+ CLOSE_PROJECT_UPDATER: ofType<{}>(),
+ UPDATE_PROJECT_SUCCESS: ofType<ProjectResource>(),
REMOVE_PROJECT: ofType<string>(),
PROJECTS_REQUEST: ofType<string>(),
PROJECTS_SUCCESS: ofType<{ projects: ProjectResource[], parentItemId?: string }>(),
value: 'payload'
});
+export const PROJECT_FORM_NAME = 'projectEditDialog';
+
export const getProjectList = (parentUuid: string = '') =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(projectActions.PROJECTS_REQUEST(parentUuid));
return services.projectService.list({
- filters: FilterBuilder
- .create()
+ filters: new FilterBuilder()
.addEqual("ownerUuid", parentUuid)
+ .getFilters()
}).then(({ items: projects }) => {
dispatch(projectActions.PROJECTS_SUCCESS({ projects, parentItemId: parentUuid }));
dispatch<any>(checkPresenceInFavorites(projects.map(project => project.uuid)));
.then(project => dispatch(projectActions.CREATE_PROJECT_SUCCESS(project)));
};
+export const updateProject = (project: Partial<ProjectResource>) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { uuid } = getState().projects.updater;
+ return services.projectService
+ .update(uuid, project)
+ .then(project => {
+ dispatch(projectActions.UPDATE_PROJECT_SUCCESS(project));
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+ dispatch<any>(getProjectList(project.ownerUuid));
+ dispatch<any>(updateDetails(project));
+ });
+ };
+
export type ProjectAction = UnionOf<typeof projectActions>;
import { projectsReducer, getTreePath } from "./project-reducer";
import { projectActions } from "./project-action";
-import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
-import { mockProjectResource } from "../../models/test-utils";
+import { TreeItem, TreeItemStatus } from "~/components/tree/tree";
+import { mockProjectResource } from "~/models/test-utils";
describe('project-reducer', () => {
creator: {
opened: false,
ownerUuid: "",
+ },
+ updater: {
+ opened: false,
+ uuid: ''
}
});
});
}],
currentItemId: "1",
creator: { opened: false, ownerUuid: "" },
+ updater: { opened: false, uuid: '' }
};
const project = {
items: [{
}],
currentItemId: "",
creator: { opened: false, ownerUuid: "" },
+ updater: { opened: false, uuid: '' }
};
const state = projectsReducer(initialState, projectActions.RESET_PROJECT_TREE_ACTIVITY(initialState.items[0].id));
status: TreeItemStatus.PENDING
}],
currentItemId: "1",
- creator: { opened: false, ownerUuid: "" }
+ creator: { opened: false, ownerUuid: "" },
+ updater: { opened: false, uuid: '' }
};
const project = {
items: [{
}],
currentItemId: "1",
creator: { opened: false, ownerUuid: "" },
+ updater: { opened: false, uuid: '' }
};
const state = projectsReducer(initialState, projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(initialState.items[0].id));
status: TreeItemStatus.PENDING,
}],
currentItemId: "1",
- creator: { opened: false, ownerUuid: "" }
+ creator: { opened: false, ownerUuid: "" },
+ updater: { opened: false, uuid: '' }
};
const project = {
items: [{
}],
currentItemId: "1",
creator: { opened: false, ownerUuid: "" },
+ updater: { opened: false, uuid: '' }
+
};
const state = projectsReducer(initialState, projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(initialState.items[0].id));
import * as _ from "lodash";
import { projectActions, ProjectAction } from "./project-action";
-import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
-import { ProjectResource } from "../../models/project";
+import { TreeItem, TreeItemStatus } from "~/components/tree/tree";
+import { ProjectResource } from "~/models/project";
export type ProjectState = {
items: Array<TreeItem<ProjectResource>>,
currentItemId: string,
- creator: ProjectCreator
+ creator: ProjectCreator,
+ updater: ProjectUpdater
};
interface ProjectCreator {
error?: string;
}
+interface ProjectUpdater {
+ opened: boolean;
+ uuid: string;
+}
+
export function findTreeItem<T>(tree: Array<TreeItem<T>>, itemId: string): TreeItem<T> | undefined {
let item;
for (const t of tree) {
}
});
+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: ''
}
};
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 => {
const items = _.cloneDeep(state.items);
import { sidePanelReducer } from "./side-panel-reducer";
import { sidePanelActions } from "./side-panel-action";
-import { ProjectsIcon } from "../../components/icon/icon";
+import { ProjectsIcon } from "~/components/icon/icon";
describe('side-panel-reducer', () => {
import * as _ from "lodash";
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 { 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 { CollectionPanelState, collectionPanelReducer } from './collection-panel/collection-panel-reducer';
import { DialogState, dialogReducer } from './dialog/dialog-reducer';
import { CollectionsState, collectionsReducer } from './collections/collections-reducer';
-import { ServiceRepository } from "../services/services";
+import { ServiceRepository } from "~/services/services";
import { treePickerReducer } from './tree-picker/tree-picker-reducer';
import { TreePicker } from './tree-picker/tree-picker';
// SPDX-License-Identifier: AGPL-3.0
import { default as unionize, ofType, UnionOf } from "unionize";
-import { TreeNode } from "../../models/tree";
import { TreePickerNode } from "./tree-picker";
export const treePickerActions = unionize({
//
// SPDX-License-Identifier: AGPL-3.0
-import { createTree, getNodeValue, getNodeChildren } from "../../models/tree";
+import { createTree, getNodeValue, getNodeChildren } from "~/models/tree";
import { TreePickerNode, createTreePickerNode } from "./tree-picker";
import { treePickerReducer } from "./tree-picker-reducer";
import { treePickerActions } from "./tree-picker-actions";
-import { TreeItemStatus } from "../../components/tree/tree";
-
+import { TreeItemStatus } from "~/components/tree/tree";
describe('TreePickerReducer', () => {
it('LOAD_TREE_PICKER_NODE - initial state', () => {
//
// SPDX-License-Identifier: AGPL-3.0
-import { createTree, setNodeValueWith, TreeNode, setNode, mapTree, mapTreeValues } from "../../models/tree";
+import { createTree, setNodeValueWith, TreeNode, setNode, mapTreeValues } from "~/models/tree";
import { TreePicker, TreePickerNode } from "./tree-picker";
import { treePickerActions, TreePickerAction } from "./tree-picker-actions";
-import { TreeItemStatus } from "../../components/tree/tree";
-
+import { TreeItemStatus } from "~/components/tree/tree";
export const treePickerReducer = (state: TreePicker = createTree(), action: TreePickerAction) =>
treePickerActions.match(action, {
id: node.id,
parent,
value: node
-});
\ No newline at end of file
+});
//
// SPDX-License-Identifier: AGPL-3.0
-import { Tree } from "../../models/tree";
-import { TreeItemStatus } from "../../components/tree/tree";
+import { Tree } from "~/models/tree";
+import { TreeItemStatus } from "~/components/tree/tree";
export type TreePicker = Tree<TreePickerNode>;
selected: false,
collapsed: true,
status: TreeItemStatus.INITIAL
-});
\ No newline at end of file
+});
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { require } from '../require';
-import { maxLength } from '../max-length';
-
-export const COLLECTION_NAME_VALIDATION = [require, maxLength(255)];
-export const COLLECTION_DESCRIPTION_VALIDATION = [maxLength(255)];
\ No newline at end of file
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { require } from '../require';
-import { maxLength } from '../max-length';
-
-export const PROJECT_NAME_VALIDATION = [require, maxLength(255)];
-export const PROJECT_DESCRIPTION_VALIDATION = [maxLength(255)];
-export const COLLECTION_NAME_VALIDATION = [require, maxLength(255)];
-export const COLLECTION_DESCRIPTION_VALIDATION = [maxLength(255)];
-export const COLLECTION_PROJECT_VALIDATION = [require];
import { maxLength } from './max-length';
export const TAG_KEY_VALIDATION = [require, maxLength(255)];
-export const TAG_VALUE_VALIDATION = [require, maxLength(255)];
\ No newline at end of file
+export const TAG_VALUE_VALIDATION = [require, maxLength(255)];
+
+export const PROJECT_NAME_VALIDATION = [require, maxLength(255)];
+export const PROJECT_DESCRIPTION_VALIDATION = [maxLength(255)];
+
+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
import { Redirect, RouteProps } from "react-router";
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";
+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";
interface ApiTokenProps {
authService: AuthService;
// SPDX-License-Identifier: AGPL-3.0
import { connect } from "react-redux";
-import { CollectionPanelFiles as Component, CollectionPanelFilesProps } from "../../components/collection-panel-files/collection-panel-files";
-import { RootState } from "../../store/store";
-import { TreeItemStatus, TreeItem } from "../../components/tree/tree";
-import { CollectionPanelFilesState, CollectionPanelDirectory, CollectionPanelFile } from "../../store/collection-panel/collection-panel-files/collection-panel-files-state";
-import { FileTreeData } from "../../components/file-tree/file-tree-data";
+import { CollectionPanelFiles as Component, CollectionPanelFilesProps } from "~/components/collection-panel-files/collection-panel-files";
+import { RootState } from "~/store/store";
+import { TreeItemStatus, TreeItem } from "~/components/tree/tree";
+import { CollectionPanelFilesState, CollectionPanelDirectory, CollectionPanelFile } from "~/store/collection-panel/collection-panel-files/collection-panel-files-state";
+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 { 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 { CollectionFileType } from "../../models/collection-file";
+import { Tree, getNodeChildren, getNode } from "~/models/tree";
+import { CollectionFileType } from "~/models/collection-file";
const memoizedMapStateToProps = () => {
let prevState: CollectionPanelFilesState;
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 } from "../../../components/icon/icon";
-import { openUpdater } from "../../../store/collections/updater/collection-updater-action";
-import { favoritePanelActions } from "../../../store/favorite-panel/favorite-panel-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 { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
export const collectionActionSet: ContextMenuActionSet = [[
{
// SPDX-License-Identifier: AGPL-3.0
import { ContextMenuActionSet } from "../context-menu-action-set";
-import { collectionPanelFilesAction } from "../../../store/collection-panel/collection-panel-files/collection-panel-files-actions";
-import { openMultipleFilesRemoveDialog } from "../../file-remove-dialog/multiple-files-remove-dialog";
-import { createCollectionWithSelected } from "../../create-collection-dialog-with-selected/create-collection-dialog-with-selected";
+import { collectionPanelFilesAction } from "~/store/collection-panel/collection-panel-files/collection-panel-files-actions";
+import { openMultipleFilesRemoveDialog } from "~/views-components/file-remove-dialog/multiple-files-remove-dialog";
+import { createCollectionWithSelected } from "~/views-components/create-collection-dialog-with-selected/create-collection-dialog-with-selected";
export const collectionFilesActionSet: ContextMenuActionSet = [[{
// SPDX-License-Identifier: AGPL-3.0
import { ContextMenuActionSet } from "../context-menu-action-set";
-import { RenameIcon, DownloadIcon, RemoveIcon } from "../../../components/icon/icon";
+import { RenameIcon, DownloadIcon, RemoveIcon } from "~/components/icon/icon";
import { openRenameFileDialog } from "../../rename-file-dialog/rename-file-dialog";
import { openFileRemoveDialog } from "../../file-remove-dialog/file-remove-dialog";
import { ContextMenuActionSet } from "../context-menu-action-set";
import { ToggleFavoriteAction } from "../actions/favorite-action";
-import { toggleFavorite } from "../../../store/favorites/favorites-actions";
-import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, RemoveIcon } from "../../../components/icon/icon";
-import { openUpdater } from "../../../store/collections/updater/collection-updater-action";
-import { favoritePanelActions } from "../../../store/favorite-panel/favorite-panel-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 { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
export const collectionResourceActionSet: ContextMenuActionSet = [[
{
import { ContextMenuActionSet } from "../context-menu-action-set";
import { ToggleFavoriteAction } from "../actions/favorite-action";
-import { toggleFavorite } from "../../../store/favorites/favorites-actions";
-import { favoritePanelActions } from "../../../store/favorite-panel/favorite-panel-action";
+import { toggleFavorite } from "~/store/favorites/favorites-actions";
+import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
export const favoriteActionSet: ContextMenuActionSet = [[{
component: ToggleFavoriteAction,
//
// SPDX-License-Identifier: AGPL-3.0
-import { reset } from "redux-form";
+import { reset, initialize } from "redux-form";
import { ContextMenuActionSet } from "../context-menu-action-set";
-import { projectActions } from "../../../store/project/project-action";
-import { NewProjectIcon } from "../../../components/icon/icon";
+import { projectActions, PROJECT_FORM_NAME } from "~/store/project/project-action";
+import { NewProjectIcon, RenameIcon } from "~/components/icon/icon";
import { ToggleFavoriteAction } from "../actions/favorite-action";
-import { toggleFavorite } from "../../../store/favorites/favorites-actions";
-import { favoritePanelActions } from "../../../store/favorite-panel/favorite-panel-action";
+import { toggleFavorite } from "~/store/favorites/favorites-actions";
+import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
import { PROJECT_CREATE_DIALOG } from "../../dialog-create/dialog-project-create";
-export const projectActionSet: ContextMenuActionSet = [[{
- icon: NewProjectIcon,
- name: "New project",
- execute: (dispatch, resource) => {
- dispatch(reset(PROJECT_CREATE_DIALOG));
- dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid }));
+export const projectActionSet: ContextMenuActionSet = [[
+ {
+ icon: NewProjectIcon,
+ name: "New project",
+ execute: (dispatch, resource) => {
+ dispatch(reset(PROJECT_CREATE_DIALOG));
+ dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid }));
+ }
+ },
+ {
+ icon: RenameIcon,
+ name: "Edit project",
+ execute: (dispatch, resource) => {
+ dispatch(projectActions.OPEN_PROJECT_UPDATER({ uuid: resource.uuid }));
+ dispatch(initialize(PROJECT_FORM_NAME, { name: resource.name, description: resource.description }));
+ }
+ },
+ {
+ component: ToggleFavoriteAction,
+ execute: (dispatch, resource) => {
+ dispatch<any>(toggleFavorite(resource)).then(() => {
+ dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
+ });
+ }
}
-}, {
- component: ToggleFavoriteAction,
- execute: (dispatch, resource) => {
- dispatch<any>(toggleFavorite(resource)).then(() => {
- dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
- });
- }
-}]];
+]];
import { ContextMenuActionSet } from "../context-menu-action-set";
import { ToggleFavoriteAction } from "../actions/favorite-action";
-import { toggleFavorite } from "../../../store/favorites/favorites-actions";
+import { toggleFavorite } from "~/store/favorites/favorites-actions";
export const resourceActionSet: ContextMenuActionSet = [[{
component: ToggleFavoriteAction,
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 { 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 { NewProjectIcon, CollectionIcon } from "../../../components/icon/icon";
+import { NewProjectIcon, CollectionIcon } from "~/components/icon/icon";
export const rootProjectActionSet: ContextMenuActionSet = [[
{
dispatch(reset(PROJECT_CREATE_DIALOG));
dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid }));
}
- },
+ },
{
icon: CollectionIcon,
name: "New Collection",
import * as React from "react";
import { ListItemIcon, ListItemText } from "@material-ui/core";
-import { AddFavoriteIcon, RemoveFavoriteIcon } from "../../../components/icon/icon";
+import { AddFavoriteIcon, RemoveFavoriteIcon } from "~/components/icon/icon";
import { connect } from "react-redux";
-import { RootState } from "../../../store/store";
+import { RootState } from "~/store/store";
const mapStateToProps = (state: RootState) => ({
isFavorite: state.contextMenu.resource !== undefined && state.favorites[state.contextMenu.resource.uuid] === true
// SPDX-License-Identifier: AGPL-3.0
import { Dispatch } from "redux";
-import { ContextMenuItem } from "../../components/context-menu/context-menu";
-import { ContextMenuResource } from "../../store/context-menu/context-menu-reducer";
+import { ContextMenuItem } from "~/components/context-menu/context-menu";
+import { ContextMenuResource } from "~/store/context-menu/context-menu-reducer";
export interface ContextMenuAction extends ContextMenuItem {
execute(dispatch: Dispatch, resource: ContextMenuResource): void;
// SPDX-License-Identifier: AGPL-3.0
import { connect } from "react-redux";
-import { RootState } from "../../store/store";
-import { contextMenuActions } from "../../store/context-menu/context-menu-actions";
-import { ContextMenu as ContextMenuComponent, ContextMenuProps, ContextMenuItem } from "../../components/context-menu/context-menu";
-import { createAnchorAt } from "../../components/popover/helpers";
-import { ContextMenuResource } from "../../store/context-menu/context-menu-reducer";
+import { RootState } from "~/store/store";
+import { contextMenuActions } from "~/store/context-menu/context-menu-actions";
+import { ContextMenu as ContextMenuComponent, ContextMenuProps, ContextMenuItem } from "~/components/context-menu/context-menu";
+import { createAnchorAt } from "~/components/popover/helpers";
+import { ContextMenuResource } from "~/store/context-menu/context-menu-reducer";
import { ContextMenuActionSet, ContextMenuAction } from "./context-menu-action-set";
import { Dispatch } from "redux";
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 { 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";
import { Dispatch } from "redux";
import { SubmissionError } from "redux-form";
-import { RootState } from "../../store/store";
+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";
+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
import { Dispatch } from "redux";
import { SubmissionError } from "redux-form";
-import { RootState } from "../../store/store";
+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";
+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
import * as React from 'react';
import { Dialog, DialogActions, DialogTitle, DialogContent, WithStyles, withStyles, StyleRulesCallback, Button, Typography, Paper } from '@material-ui/core';
-import { ArvadosTheme } from '../../common/custom-theme';
+import { ArvadosTheme } from '~/common/custom-theme';
type CssRules = 'link' | 'paper' | 'button';
});
interface CurrentTokenDataProps {
- currentToken?: string;
+ currentToken?: string;
open: boolean;
}
type CurrentTokenProps = CurrentTokenDataProps & CurrentTokenActionProps & WithStyles<CssRules>;
-export const CurrentTokenDialog = withStyles(styles)(
+export const CurrentTokenDialog = withStyles(styles)(
class extends React.Component<CurrentTokenProps> {
-
+
render() {
const { classes, open, handleClose, currentToken } = this.props;
return (
<DialogTitle>Current Token</DialogTitle>
<DialogContent>
<Typography variant='body1' paragraph={true}>
- The Arvados API token is a secret key that enables the Arvados SDKs to access Arvados with the proper permissions.
+ The Arvados API token is a secret key that enables the Arvados SDKs to access Arvados with the proper permissions.
<Typography component='p'>
For more information see
<a href='http://doc.arvados.org/user/reference/api-tokens.html' target='blank' className={classes.link}>
</Typography>
</Typography>
- <Typography variant='body1' paragraph={true}>
+ <Typography variant='body1' paragraph={true}>
Paste the following lines at a shell prompt to set up the necessary environment for Arvados SDKs to authenticate to your klingenc account.
</Typography>
<Paper className={classes.paper} elevation={0}>
<Typography variant='body1'>
- HISTIGNORE=$HISTIGNORE:'export ARVADOS_API_TOKEN=*'
+ HISTIGNORE=$HISTIGNORE:'export ARVADOS_API_TOKEN=*'
</Typography>
<Typography variant='body1'>
export ARVADOS_API_TOKEN={currentToken}
</Typography>
</Paper>
<Typography variant='body1'>
- Arvados
- <a href='http://doc.arvados.org/user/reference/api-tokens.html' target='blank' className={classes.link}>virtual machines</a>
+ Arvados
+ <a href='http://doc.arvados.org/user/reference/api-tokens.html' target='blank' className={classes.link}>virtual machines</a>
do this for you automatically. This setup is needed only when you use the API remotely (e.g., from your own workstation).
</Typography>
</DialogContent>
);
}
}
-);
\ No newline at end of file
+);
// SPDX-License-Identifier: AGPL-3.0
import { connect } from "react-redux";
-import { RootState } from "../../store/store";
-import { DataExplorer as DataExplorerComponent } from "../../components/data-explorer/data-explorer";
-import { getDataExplorer } from "../../store/data-explorer/data-explorer-reducer";
+import { RootState } from "~/store/store";
+import { DataExplorer as DataExplorerComponent } from "~/components/data-explorer/data-explorer";
+import { getDataExplorer } from "~/store/data-explorer/data-explorer-reducer";
import { Dispatch } from "redux";
-import { dataExplorerActions } from "../../store/data-explorer/data-explorer-action";
-import { DataColumn } from "../../components/data-table/data-column";
-import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
-import { DataColumns } from "../../components/data-table/data-table";
+import { dataExplorerActions } from "~/store/data-explorer/data-explorer-action";
+import { DataColumn } from "~/components/data-table/data-column";
+import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters";
+import { DataColumns } from "~/components/data-table/data-table";
interface Props {
id: string;
import * as React from 'react';
import { Grid, Typography } from '@material-ui/core';
import { FavoriteStar } from '../favorite-star/favorite-star';
-import { ResourceKind } from '../../models/resource';
-import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon } from '../../components/icon/icon';
-import { formatDate, formatFileSize } from '../../common/formatters';
-import { resourceLabel } from '../../common/labels';
+import { ResourceKind } from '~/models/resource';
+import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon } from '~/components/icon/icon';
+import { formatDate, formatFileSize } from '~/common/formatters';
+import { resourceLabel } from '~/common/labels';
export const renderName = (item: {name: string; uuid: string, kind: string}) =>
export const renderStatus = (item: {status?: string}) =>
<Typography noWrap align="center" >
{item.status || "-"}
- </Typography>;
\ No newline at end of file
+ </Typography>;
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { CollectionIcon } from '../../components/icon/icon';
-import { CollectionResource } from '../../models/collection';
-import { formatDate } from '../../common/formatters';
-import { resourceLabel } from '../../common/labels';
-import { ResourceKind } from '../../models/resource';
+import { CollectionIcon } from '~/components/icon/icon';
+import { CollectionResource } from '~/models/collection';
+import { formatDate } from '~/common/formatters';
+import { resourceLabel } from '~/common/labels';
+import { ResourceKind } from '~/models/resource';
import { DetailsData } from "./details-data";
-import { DetailsAttribute } from "../../components/details-attribute/details-attribute";
+import { DetailsAttribute } from "~/components/details-attribute/details-attribute";
export class CollectionDetails extends DetailsData<CollectionResource> {
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { DetailsResource } from "../../models/details";
+import { DetailsResource } from "~/models/details";
export abstract class DetailsData<T extends DetailsResource = DetailsResource> {
constructor(protected item: T) {}
import * as React from 'react';
import { Drawer, IconButton, Tabs, Tab, Typography, Grid } from '@material-ui/core';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import { ArvadosTheme } from '../../common/custom-theme';
+import { ArvadosTheme } from '~/common/custom-theme';
import * as classnames from "classnames";
import { connect } from 'react-redux';
-import { RootState } from '../../store/store';
-import { detailsPanelActions } from "../../store/details-panel/details-panel-action";
-import { CloseIcon } from '../../components/icon/icon';
-import { EmptyResource } from '../../models/empty';
+import { RootState } from '~/store/store';
+import { detailsPanelActions } from "~/store/details-panel/details-panel-action";
+import { CloseIcon } from '~/components/icon/icon';
+import { EmptyResource } from '~/models/empty';
import { Dispatch } from "redux";
-import { ResourceKind } from "../../models/resource";
+import { ResourceKind } from "~/models/resource";
import { ProjectDetails } from "./project-details";
import { CollectionDetails } from "./collection-details";
import { ProcessDetails } from "./process-details";
import { EmptyDetails } from "./empty-details";
import { DetailsData } from "./details-data";
-import { DetailsResource } from "../../models/details";
+import { DetailsResource } from "~/models/details";
type CssRules = 'drawerPaper' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'tabContainer';
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { DefaultIcon, ProjectsIcon } from '../../components/icon/icon';
-import { EmptyResource } from '../../models/empty';
+import { DefaultIcon, ProjectsIcon } from '~/components/icon/icon';
+import { EmptyResource } from '~/models/empty';
import { DetailsData } from "./details-data";
-import { DefaultView } from '../../components/default-view/default-view';
+import { DefaultView } from '~/components/default-view/default-view';
export class EmptyDetails extends DetailsData<EmptyResource> {
getIcon(className?: string) {
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { ProcessIcon } from '../../components/icon/icon';
-import { ProcessResource } from '../../models/process';
-import { formatDate } from '../../common/formatters';
-import { ResourceKind } from '../../models/resource';
-import { resourceLabel } from '../../common/labels';
+import { ProcessIcon } from '~/components/icon/icon';
+import { ProcessResource } from '~/models/process';
+import { formatDate } from '~/common/formatters';
+import { ResourceKind } from '~/models/resource';
+import { resourceLabel } from '~/common/labels';
import { DetailsData } from "./details-data";
-import { DetailsAttribute } from "../../components/details-attribute/details-attribute";
+import { DetailsAttribute } from "~/components/details-attribute/details-attribute";
export class ProcessDetails extends DetailsData<ProcessResource> {
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { ProjectIcon } from '../../components/icon/icon';
-import { ProjectResource } from '../../models/project';
-import { formatDate } from '../../common/formatters';
-import { ResourceKind } from '../../models/resource';
-import { resourceLabel } from '../../common/labels';
+import { ProjectIcon } from '~/components/icon/icon';
+import { ProjectResource } from '~/models/project';
+import { formatDate } from '~/common/formatters';
+import { ResourceKind } from '~/models/resource';
+import { resourceLabel } from '~/common/labels';
import { DetailsData } from "./details-data";
-import { DetailsAttribute } from "../../components/details-attribute/details-attribute";
+import { DetailsAttribute } from "~/components/details-attribute/details-attribute";
export class ProjectDetails extends DetailsData<ProjectResource> {
import * as React from "react";
import { InjectedFormProps, Field, WrappedFieldProps } from "redux-form";
-import { Dialog, DialogTitle, DialogContent, DialogActions, Button, CircularProgress, FormHelperText } 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/create-project/create-project-validator";
+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 }>) =>
import * as React from 'react';
import { reduxForm, Field } from 'redux-form';
import { compose } from 'redux';
-import { TextField } from '../../components/text-field/text-field';
+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 { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-collection/create-collection-validator';
-import { FileUpload } from "../../components/file-upload/file-upload";
+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";
+import { RootState } from "~/store/store";
+import { collectionUploaderActions, UploadFile } from "~/store/collections/uploader/collection-uploader-actions";
-type CssRules = "button" | "lastButton" | "formContainer" | "textField" | "createProgress" | "dialogActions";
+type CssRules = "button" | "lastButton" | "formContainer" | "createProgress" | "dialogActions";
const styles: StyleRulesCallback<CssRules> = theme => ({
button: {
display: "flex",
flexDirection: "column",
},
- textField: {
- marginBottom: theme.spacing.unit * 3
- },
createProgress: {
position: "absolute",
minWidth: "20px",
}
});
-interface DialogCollectionCreateProps {
+interface DialogCollectionDataProps {
open: boolean;
- handleClose: () => void;
- onSubmit: (data: { name: string, description: string }, files: UploadFile[]) => void;
handleSubmit: any;
submitting: boolean;
invalid: 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(
})),
reduxForm({ form: COLLECTION_CREATE_DIALOG }),
withStyles(styles))(
- class DialogCollectionCreate extends React.Component<DialogCollectionCreateProps & DispatchProp & WithStyles<CssRules>> {
+ class DialogCollectionCreate extends React.Component<DialogCollectionProps> {
render() {
const { classes, open, handleClose, handleSubmit, onSubmit, submitting, invalid, pristine, files } = this.props;
const busy = submitting || files.reduce(
disabled={submitting}
component={TextField}
validate={COLLECTION_NAME_VALIDATION}
- className={classes.textField}
label="Collection Name" />
<Field name="description"
disabled={submitting}
component={TextField}
validate={COLLECTION_DESCRIPTION_VALIDATION}
- className={classes.textField}
label="Description - optional" />
<FileUpload
files={files}
import * as React from 'react';
import { reduxForm, Field } from 'redux-form';
import { compose } from 'redux';
-import { TextField } from '../../components/text-field/text-field';
+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 { PROJECT_NAME_VALIDATION, PROJECT_DESCRIPTION_VALIDATION } from '../../validators/create-project/create-project-validator';
+import { PROJECT_NAME_VALIDATION, PROJECT_DESCRIPTION_VALIDATION } from '~/validators/validators';
-type CssRules = "button" | "lastButton" | "formContainer" | "textField" | "dialog" | "dialogTitle" | "createProgress" | "dialogActions";
+type CssRules = "button" | "lastButton" | "formContainer" | "dialog" | "dialogTitle" | "createProgress" | "dialogActions";
const styles: StyleRulesCallback<CssRules> = theme => ({
button: {
dialogTitle: {
paddingBottom: "0"
},
- textField: {
- marginTop: "32px",
- },
dialog: {
minWidth: "600px",
minHeight: "320px"
<Field name="name"
component={TextField}
validate={PROJECT_NAME_VALIDATION}
- className={classes.textField}
label="Project Name"/>
<Field name="description"
component={TextField}
validate={PROJECT_DESCRIPTION_VALIDATION}
- className={classes.textField}
label="Description - optional"/>
</DialogContent>
<DialogActions className={classes.dialogActions}>
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, TextField, StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress } from '@material-ui/core';
-import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-collection/create-collection-validator';
-import { COLLECTION_FORM_NAME } from '../../store/collections/updater/collection-updater-action';
+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' | 'textField' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
+type CssRules = 'content' | 'actions' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
content: {
padding: `${theme.spacing.unit}px ${theme.spacing.unit * 3 - theme.spacing.unit / 2}px
${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px`
},
- textField: {
- marginBottom: theme.spacing.unit * 3
- },
buttonWrapper: {
position: 'relative'
},
type DialogCollectionProps = DialogCollectionDataProps & DialogCollectionAction & WithStyles<CssRules>;
-interface TextFieldProps {
- label: string;
- floatinglabeltext: string;
- className?: string;
- input?: string;
- meta?: any;
-}
-
export const DialogCollectionUpdate = compose(
reduxForm({ form: COLLECTION_FORM_NAME }),
withStyles(styles))(
<form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
<DialogTitle>Edit Collection</DialogTitle>
<DialogContent className={classes.content}>
- <Field name="name"
+ <Field name='name'
disabled={submitting}
- component={this.renderTextField}
- floatinglabeltext="Collection Name"
+ component={TextField}
validate={COLLECTION_NAME_VALIDATION}
- className={classes.textField}
label="Collection Name" />
- <Field name="description"
+ <Field name='description'
disabled={submitting}
- component={this.renderTextField}
- floatinglabeltext="Description - optional"
+ component={TextField}
validate={COLLECTION_DESCRIPTION_VALIDATION}
- className={classes.textField}
label="Description - optional" />
</DialogContent>
<DialogActions className={classes.actions}>
</Dialog>
);
}
-
- renderTextField = ({ input, label, meta: { touched, error }, ...custom }: TextFieldProps) => (
- <TextField
- helperText={touched && error}
- label={label}
- className={this.props.classes.textField}
- error={touched && !!error}
- autoComplete='off'
- {...input}
- {...custom}
- />
- )
}
);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// 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>;
+ }
+ }
+ );
// SPDX-License-Identifier: AGPL-3.0
import * as React from "react";
-import { FavoriteIcon } from "../../components/icon/icon";
+import { FavoriteIcon } from "~/components/icon/icon";
import { connect } from "react-redux";
-import { RootState } from "../../store/store";
+import { RootState } from "~/store/store";
import { withStyles, StyleRulesCallback, WithStyles } from "@material-ui/core";
type CssRules = "icon";
export const FavoriteStar = connect(mapStateToProps)(
withStyles(styles)((props: { visible: boolean; className?: string; } & WithStyles<CssRules>) =>
props.visible ? <FavoriteIcon className={props.className || props.classes.icon} /> : null
- ));
\ No newline at end of file
+ ));
import { Dispatch } from "redux";
import { connect } from "react-redux";
-import { ConfirmationDialog } from "../../components/confirmation-dialog/confirmation-dialog";
-import { withDialog } from "../../store/dialog/with-dialog";
-import { dialogActions } from "../../store/dialog/dialog-actions";
-import { snackbarActions } from "../../store/snackbar/snackbar-actions";
+import { ConfirmationDialog } from "~/components/confirmation-dialog/confirmation-dialog";
+import { withDialog } from "~/store/dialog/with-dialog";
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { snackbarActions } from "~/store/snackbar/snackbar-actions";
const FILE_REMOVE_DIALOG = 'fileRemoveDialog';
export const [FileRemoveDialog] = [ConfirmationDialog]
.map(withDialog(FILE_REMOVE_DIALOG))
- .map(connect(undefined, mapDispatchToProps));
\ No newline at end of file
+ .map(connect(undefined, mapDispatchToProps));
import { Dispatch } from "redux";
import { connect } from "react-redux";
-import { ConfirmationDialog } from "../../components/confirmation-dialog/confirmation-dialog";
-import { withDialog } from "../../store/dialog/with-dialog";
-import { dialogActions } from "../../store/dialog/dialog-actions";
-import { snackbarActions } from "../../store/snackbar/snackbar-actions";
+import { ConfirmationDialog } from "~/components/confirmation-dialog/confirmation-dialog";
+import { withDialog } from "~/store/dialog/with-dialog";
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { snackbarActions } from "~/store/snackbar/snackbar-actions";
const MULTIPLE_FILES_REMOVE_DIALOG = 'multipleFilesRemoveDialog';
export const [MultipleFilesRemoveDialog] = [ConfirmationDialog]
.map(withDialog(MULTIPLE_FILES_REMOVE_DIALOG))
- .map(connect(undefined, mapDispatchToProps));
\ No newline at end of file
+ .map(connect(undefined, mapDispatchToProps));
// SPDX-License-Identifier: AGPL-3.0
import * as React from "react";
-import { mount, configure, ReactWrapper } from "enzyme";
+import { mount, configure } from "enzyme";
import * as Adapter from "enzyme-adapter-react-16";
import { MainAppBar } from "./main-app-bar";
-import { SearchBar } from "../../components/search-bar/search-bar";
-import { Breadcrumbs } from "../../components/breadcrumbs/breadcrumbs";
-import { DropdownMenu } from "../../components/dropdown-menu/dropdown-menu";
+import { SearchBar } from "~/components/search-bar/search-bar";
+import { Breadcrumbs } from "~/components/breadcrumbs/breadcrumbs";
+import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
import { Button, MenuItem, IconButton } from "@material-ui/core";
-import { User } from "../../models/user";
+import { User } from "~/models/user";
configure({ adapter: new Adapter() });
import * as React from "react";
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";
+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";
export interface MainAppBarMenuItem {
label: string;
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 { 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 { 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 { TreeProps, 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 { createTreePickerNode } from "~/store/tree-picker/tree-picker";
+import { RootState } from "~/store/store";
+import { ServiceRepository } from "~/services/services";
+import { FilterBuilder } from "~/common/api/filter-builder";
type ProjectTreePickerProps = Pick<TreeProps<ProjectResource>, 'toggleItemActive' | 'toggleItemOpen'>;
const ownerUuid = id.length === 0 ? services.authService.getUuid() || '' : id;
- const filters = FilterBuilder
- .create()
- .addEqual('ownerUuid', ownerUuid);
+ const filters = new FilterBuilder()
+ .addEqual('ownerUuid', ownerUuid)
+ .getFilters();
const { items } = await services.projectService.list({ filters });
nodes: projects.map(project => createTreePickerNode({ id: project.uuid, value: project }))
}));
dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id }));
- };
\ No newline at end of file
+ };
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 } from '~/components/tree/tree';
+import { ProjectResource } from '~/models/project';
+import { mockProjectResource } from '~/models/test-utils';
Enzyme.configure({ adapter: new Adapter() });
import * as React from 'react';
import { ReactElement } from 'react';
-import { StyleRulesCallback, Theme, WithStyles, withStyles } from '@material-ui/core/styles';
-import { Tree, TreeItem, TreeItemStatus } from '../../components/tree/tree';
-import { ProjectResource } from '../../models/project';
-import { ProjectIcon } from '../../components/icon/icon';
-import { ArvadosTheme } from '../../common/custom-theme';
-import { ListItemTextIcon } from '../../components/list-item-text-icon/list-item-text-icon';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import { Tree, TreeItem, TreeItemStatus } from '~/components/tree/tree';
+import { ProjectResource } from '~/models/project';
+import { ProjectIcon } from '~/components/icon/icon';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { ListItemTextIcon } from '~/components/list-item-text-icon/list-item-text-icon';
type CssRules = 'root';
import * as React from "react";
import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from "@material-ui/core";
-import { withDialog } from "../../store/dialog/with-dialog";
-import { dialogActions } from "../../store/dialog/dialog-actions";
+import { withDialog } from "~/store/dialog/with-dialog";
+import { dialogActions } from "~/store/dialog/dialog-actions";
export const REMOVE_DIALOG = 'removeCollectionFilesDialog';
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 { withDialog } from "~/store/dialog/with-dialog";
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { RenameDialog } from "~/components/rename-dialog/rename-dialog";
export const RENAME_FILE_DIALOG = 'renameFileDialog';
import * as React from "react";
import { connect } from "react-redux";
-import { RootState } from "../../store/store";
+import { RootState } from "~/store/store";
import MaterialSnackbar, { SnackbarProps } from "@material-ui/core/Snackbar";
import { Dispatch } from "redux";
-import { snackbarActions } from "../../store/snackbar/snackbar-actions";
+import { snackbarActions } from "~/store/snackbar/snackbar-actions";
const mapStateToProps = (state: RootState): SnackbarProps => ({
anchorOrigin: { vertical: "bottom", horizontal: "center" },
// SPDX-License-Identifier: AGPL-3.0
import { connect } from "react-redux";
-import { Tree, TreeProps, TreeItem } 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 { Tree, TreeProps, TreeItem } 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";
const memoizedMapStateToProps = () => {
let prevState: TTreePicker;
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 { 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) => ({
--- /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);
} from '@material-ui/core';
import { connect, DispatchProp } from "react-redux";
import { RouteComponentProps } from 'react-router';
-import { ArvadosTheme } from '../../common/custom-theme';
-import { RootState } from '../../store/store';
-import { MoreOptionsIcon, CollectionIcon, CopyIcon } from '../../components/icon/icon';
-import { DetailsAttribute } from '../../components/details-attribute/details-attribute';
-import { CollectionResource } from '../../models/collection';
-import { CollectionPanelFiles } from '../../views-components/collection-panel-files/collection-panel-files';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { RootState } from '~/store/store';
+import { MoreOptionsIcon, CollectionIcon, CopyIcon } from '~/components/icon/icon';
+import { DetailsAttribute } from '~/components/details-attribute/details-attribute';
+import { CollectionResource } from '~/models/collection';
+import { CollectionPanelFiles } from '~/views-components/collection-panel-files/collection-panel-files';
import * as CopyToClipboard from 'react-copy-to-clipboard';
-import { TagResource } from '../../models/tag';
+import { TagResource } from '~/models/tag';
import { CollectionTagForm } from './collection-tag-form';
-import { deleteCollectionTag } from '../../store/collection-panel/collection-panel-action';
+import { deleteCollectionTag } from '~/store/collection-panel/collection-panel-action';
type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'value';
export const CollectionPanel = withStyles(styles)(
- connect((state: RootState) => ({
- item: state.collectionPanel.item,
- tags: state.collectionPanel.tags
+ connect((state: RootState) => ({
+ item: state.collectionPanel.item,
+ tags: state.collectionPanel.tags
}))(
- class extends React.Component<CollectionPanelProps> {
+ class extends React.Component<CollectionPanelProps> {
render() {
const { classes, item, tags, onContextMenu } = this.props;
<CardContent>
<Grid container direction="column">
<Grid item xs={6}>
- <DetailsAttribute classValue={classes.value}
- label='Collection UUID'
+ <DetailsAttribute classValue={classes.value}
+ label='Collection UUID'
value={item && item.uuid}>
<CopyToClipboard text={item && item.uuid}>
<CopyIcon className={classes.copyIcon} />
</Card>
<Card className={classes.card}>
- <CardHeader title="Tags" />
+ <CardHeader title="Properties" />
<CardContent>
<Grid container direction="column">
<Grid item xs={12}><CollectionTagForm /></Grid>
import * as React from 'react';
import { reduxForm, Field, reset } from 'redux-form';
import { compose, Dispatch } from 'redux';
-import { ArvadosTheme } from '../../common/custom-theme';
-import { StyleRulesCallback, withStyles, WithStyles, TextField, Button, CircularProgress } from '@material-ui/core';
-import { TagProperty } from '../../models/tag';
-import { createCollectionTag, COLLECTION_TAG_FORM_NAME } from '../../store/collection-panel/collection-panel-action';
-import { TAG_VALUE_VALIDATION, TAG_KEY_VALIDATION } from '../../validators/validators';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress, Grid } from '@material-ui/core';
+import { TagProperty } from '~/models/tag';
+import { TextField } from '~/components/text-field/text-field';
+import { createCollectionTag, COLLECTION_TAG_FORM_NAME } from '~/store/collection-panel/collection-panel-action';
+import { TAG_VALUE_VALIDATION, TAG_KEY_VALIDATION } from '~/validators/validators';
-type CssRules = 'form' | 'textField' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
+type CssRules = 'buttonWrapper' | 'saveButton' | 'circularProgress';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- form: {
- marginBottom: theme.spacing.unit * 4
- },
- textField: {
- marginRight: theme.spacing.unit
- },
buttonWrapper: {
position: 'relative',
display: 'inline-block'
handleSubmit: any;
}
-interface TextFieldProps {
- label: string;
- floatinglabeltext: string;
- className?: string;
- input?: string;
- meta?: any;
-}
-
type CollectionTagFormProps = CollectionTagFormDataProps & CollectionTagFormActionProps & WithStyles<CssRules>;
export const CollectionTagForm = compose(
- reduxForm({
- form: COLLECTION_TAG_FORM_NAME,
+ reduxForm({
+ form: COLLECTION_TAG_FORM_NAME,
onSubmit: (data: TagProperty, dispatch: Dispatch) => {
dispatch<any>(createCollectionTag(data));
dispatch(reset(COLLECTION_TAG_FORM_NAME));
- }
+ }
}),
withStyles(styles))(
-
- class CollectionTagForm extends React.Component<CollectionTagFormProps> {
+
+ class CollectionTagForm extends React.Component<CollectionTagFormProps> {
render() {
const { classes, submitting, pristine, invalid, handleSubmit } = this.props;
return (
- <form className={classes.form} onSubmit={handleSubmit}>
- <Field name="key"
- disabled={submitting}
- component={this.renderTextField}
- floatinglabeltext="Key"
- validate={TAG_KEY_VALIDATION}
- className={classes.textField}
- label="Key" />
- <Field name="value"
- disabled={submitting}
- component={this.renderTextField}
- floatinglabeltext="Value"
- validate={TAG_VALUE_VALIDATION}
- className={classes.textField}
- label="Value" />
- <div className={classes.buttonWrapper}>
- <Button type="submit" className={classes.saveButton}
- color="primary"
- size='small'
- disabled={invalid || submitting || pristine}
- variant="contained">
- ADD
- </Button>
- {submitting && <CircularProgress size={20} className={classes.circularProgress} />}
- </div>
+ <form onSubmit={handleSubmit}>
+ <Grid container justify="flex-start" alignItems="baseline" spacing={24}>
+ <Grid item xs={3} component={"span"}>
+ <Field name="key"
+ disabled={submitting}
+ component={TextField}
+ validate={TAG_KEY_VALIDATION}
+ label="Key" />
+ </Grid>
+ <Grid item xs={5} component={"span"}>
+ <Field name="value"
+ disabled={submitting}
+ component={TextField}
+ validate={TAG_VALUE_VALIDATION}
+ label="Value" />
+ </Grid>
+ <Grid item component={"span"} className={classes.buttonWrapper}>
+ <Button type="submit" className={classes.saveButton}
+ color="primary"
+ size='small'
+ disabled={invalid || submitting || pristine}
+ variant="contained">
+ ADD
+ </Button>
+ {submitting && <CircularProgress size={20} className={classes.circularProgress} />}
+ </Grid>
+ </Grid>
</form>
);
}
-
- renderTextField = ({ input, label, meta: { touched, error }, ...custom }: TextFieldProps) => (
- <TextField
- helperText={touched && error}
- label={label}
- className={this.props.classes.textField}
- error={touched && !!error}
- autoComplete='off'
- {...input}
- {...custom}
- />
- )
-
}
- );
\ No newline at end of file
+ );
//
// SPDX-License-Identifier: AGPL-3.0
-import { GroupContentsResource } from "../../services/groups-service/groups-service";
-import { ResourceKind } from "../../models/resource";
+import { GroupContentsResource } from "~/services/groups-service/groups-service";
+import { ResourceKind } from "~/models/resource";
export interface FavoritePanelItem {
uuid: string;
status?: string;
}
-
export function resourceToDataItem(r: GroupContentsResource): FavoritePanelItem {
return {
uuid: r.uuid,
status: r.kind === ResourceKind.PROCESS ? r.state : undefined
};
}
-
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 { DataExplorer } from "~/views-components/data-explorer/data-explorer";
import { DispatchProp, connect } from 'react-redux';
-import { DataColumns } from '../../components/data-table/data-table';
+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 { ContainerRequestState } from '../../models/container-request';
-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 { FavoriteIcon } from '../../components/icon/icon';
+import { RootState } from '~/store/store';
+import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
+import { ContainerRequestState } from '~/models/container-request';
+import { SortDirection } from '~/components/data-table/data-column';
+import { ResourceKind } 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 { FavoriteIcon } from '~/components/icon/icon';
type CssRules = "toolbar" | "button";
selected: true,
configurable: true,
sortDirection: SortDirection.ASC,
+ filters: [],
render: renderName,
width: "450px"
},
name: "Status",
selected: true,
configurable: true,
+ sortDirection: SortDirection.NONE,
filters: [
{
name: ContainerRequestState.COMMITTED,
name: FavoritePanelColumnNames.TYPE,
selected: true,
configurable: true,
+ sortDirection: SortDirection.NONE,
filters: [
{
name: resourceLabel(ResourceKind.COLLECTION),
name: FavoritePanelColumnNames.OWNER,
selected: true,
configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: [],
render: item => renderOwner(item.owner),
width: "200px"
},
name: FavoritePanelColumnNames.FILE_SIZE,
selected: true,
configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: [],
render: item => renderFileSize(item.fileSize),
width: "50px"
},
selected: true,
configurable: true,
sortDirection: SortDirection.NONE,
+ filters: [],
render: item => renderDate(item.lastModified),
width: "150px"
}
onRowClick={this.props.onItemClick}
onRowDoubleClick={this.props.onItemDoubleClick}
onContextMenu={this.props.onContextMenu}
- extractKey={(item: FavoritePanelItem) => item.uuid}
+ extractKey={(item: FavoritePanelItem) => item.uuid}
defaultIcon={FavoriteIcon}
defaultMessages={['Your favorites list is empty.']}/>
;
//
// SPDX-License-Identifier: AGPL-3.0
-import { GroupContentsResource } from "../../services/groups-service/groups-service";
-import { ResourceKind } from "../../models/resource";
+import { GroupContentsResource } from "~/services/groups-service/groups-service";
+import { ResourceKind } from "~/models/resource";
export interface ProjectPanelItem {
uuid: string;
status?: string;
}
-
export function resourceToDataItem(r: GroupContentsResource): ProjectPanelItem {
return {
uuid: r.uuid,
status: r.kind === ResourceKind.PROCESS ? r.state : undefined
};
}
-
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 { DataExplorer } from "~/views-components/data-explorer/data-explorer";
import { DispatchProp, connect } from 'react-redux';
-import { DataColumns } from '../../components/data-table/data-table';
+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 { ContainerRequestState } from '../../models/container-request';
-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 { restoreBranch } from '../../store/navigation/navigation-action';
-import { ProjectIcon } from '../../components/icon/icon';
+import { RootState } from '~/store/store';
+import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
+import { ContainerRequestState } from '~/models/container-request';
+import { SortDirection } from '~/components/data-table/data-column';
+import { ResourceKind } 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 { ProjectIcon } from '~/components/icon/icon';
type CssRules = 'root' | "toolbar" | "button";
selected: true,
configurable: true,
sortDirection: SortDirection.ASC,
+ filters: [],
render: renderName,
width: "450px"
},
name: "Status",
selected: true,
configurable: true,
+ sortDirection: SortDirection.NONE,
filters: [
{
name: ContainerRequestState.COMMITTED,
name: ProjectPanelColumnNames.TYPE,
selected: true,
configurable: true,
+ sortDirection: SortDirection.NONE,
filters: [
{
name: resourceLabel(ResourceKind.COLLECTION),
name: ProjectPanelColumnNames.OWNER,
selected: true,
configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: [],
render: item => renderOwner(item.owner),
width: "200px"
},
name: ProjectPanelColumnNames.FILE_SIZE,
selected: true,
configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: [],
render: item => renderFileSize(item.fileSize),
width: "50px"
},
selected: true,
configurable: true,
sortDirection: SortDirection.NONE,
+ filters: [],
render: item => renderDate(item.lastModified),
width: "150px"
}
import * as React from 'react';
import * as ReactDOM from 'react-dom';
-import { Workbench } from '../../views/workbench/workbench';
+import { Workbench } from './workbench';
import { Provider } from "react-redux";
-import { configureStore } from "../../store/store";
+import { configureStore } from "~/store/store";
import createBrowserHistory from "history/createBrowserHistory";
import { ConnectedRouter } from "react-router-redux";
import { MuiThemeProvider } from '@material-ui/core/styles';
-import { CustomTheme } from '../../common/custom-theme';
-import { createServices } from "../../services/services";
-import { AuthService } from "../../services/auth-service/auth-service";
-import Axios from "axios";
+import { CustomTheme } from '~/common/custom-theme';
+import { createServices } from "~/services/services";
const history = createBrowserHistory();
-const authService = new AuthService(Axios.create(), '/arvados/v1');
-
-authService.getUuid = jest.fn().mockReturnValueOnce('test');
it('renders without crashing', () => {
const div = document.createElement('div');
- const store = configureStore(createBrowserHistory(), createServices("/arvados/v1"));
+ const services = createServices("/arvados/v1");
+ services.authService.getUuid = jest.fn().mockReturnValueOnce('test');
+ const store = configureStore(createBrowserHistory(), services);
ReactDOM.render(
<MuiThemeProvider theme={CustomTheme}>
<Provider store={store}>
<ConnectedRouter history={history}>
- <Workbench authService={authService} />
+ <Workbench authService={services.authService}/>
</ConnectedRouter>
</Provider>
</MuiThemeProvider>,
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
import Drawer from '@material-ui/core/Drawer';
import { connect, DispatchProp } from "react-redux";
-import { Route, Switch, RouteComponentProps, Redirect } 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 { Route, RouteComponentProps, Switch, Redirect } 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 "../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 { 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 { SidePanelIdentifiers } from '../../store/side-panel/side-panel-reducer';
-import { ProjectResource } from '../../models/project';
-import { ResourceKind } from '../../models/resource';
-import { ContextMenu, ContextMenuKind } from "../../views-components/context-menu/context-menu";
+import { detailsPanelActions, loadDetails } from "~/store/details-panel/details-panel-action";
+import { contextMenuActions } from "~/store/context-menu/context-menu-actions";
+import { SidePanelIdentifiers } from '~/store/side-panel/side-panel-reducer';
+import { ProjectResource } from '~/models/project';
+import { ResourceKind } from '~/models/resource';
+import { ContextMenu, ContextMenuKind } 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 { 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 { 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 { 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';
const DRAWER_WITDH = 240;
const APP_BAR_HEIGHT = 100;
<FileRemoveDialog />
<MultipleFilesRemoveDialog />
<UpdateCollectionDialog />
+ <UpdateProjectDialog />
<CurrentTokenDialog
currentToken={this.props.currentToken}
open={this.state.isCurrentTokenDialogOpen}
);
}
- renderCollectionPanel = (props: RouteComponentProps<{ id: string }>) => <CollectionPanel
+ renderCollectionPanel = (props: RouteComponentProps<{ id: string }>) => <CollectionPanel
onItemRouteChange={(collectionId) => {
this.props.dispatch<any>(loadCollection(collectionId, ResourceKind.COLLECTION));
this.props.dispatch<any>(loadCollectionTags(collectionId));
} else {
kind = ContextMenuKind.RESOURCE;
}
-
+
this.openContextMenu(event, {
uuid: item.uuid,
name: item.name,
case ResourceKind.COLLECTION:
this.props.dispatch(loadCollection(item.uuid, item.kind as ResourceKind));
this.props.dispatch(push(getCollectionUrl(item.uuid)));
- default:
+ default:
this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
}
{
"compilerOptions": {
- "baseUrl": ".",
+ "baseUrl": "./",
"outDir": "build/dist",
"module": "esnext",
"target": "es5",
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": false,
"experimentalDecorators": true,
- "emitDecoratorMetadata": true
- // waits for moduleNameMapper being able to override
- // "paths": {
- // "~/*": ["src/*"]
- // }
+ "emitDecoratorMetadata": true,
+ "paths": {
+ "~/*": ["src/*"]
+ }
},
"exclude": [
"node_modules",
prop-types "^15.6.1"
warning "^4.0.1"
-react-scripts-ts@2.16.0:
- version "2.16.0"
- resolved "https://registry.yarnpkg.com/react-scripts-ts/-/react-scripts-ts-2.16.0.tgz#45f831a12139c3b59d6bb729c1b6ef51e0f22908"
+react-scripts-ts@2.17.0:
+ version "2.17.0"
+ resolved "https://registry.yarnpkg.com/react-scripts-ts/-/react-scripts-ts-2.17.0.tgz#398bae19a30c9b39b3dfe0720ebb40c60c2f6574"
dependencies:
autoprefixer "7.1.6"
babel-jest "^22.1.0"