#
# SPDX-License-Identifier: AGPL-3.0
+REACT_APP_ARVADOS_CONFIG_URL=/config.json
REACT_APP_ARVADOS_API_HOST=https://qr1hi.arvadosapi.com
\ No newline at end of file
README.md
public/*
.licenseignore
+.yarnrc
+.npmrc
--- /dev/null
+save-exact=true
--- /dev/null
+save-prefix false
--iteration "$(ITERATION)" \
--maintainer="$(MAINTAINER)" \
--description="$(DESCRIPTION)" \
- --deb-no-default-config-files \
+ --config-files="etc/arvados/workbench2/workbench2.example.json" \
$(WORKSPACE)/build/=$(DEST_DIR)
$(RPM_FILE): build
--iteration "$(ITERATION)" \
--maintainer="$(MAINTAINER)" \
--description="$(DESCRIPTION)" \
+ --config-files="etc/arvados/workbench2/workbench2.example.json" \
$(WORKSPACE)/build/=$(DEST_DIR)
copy: $(DEB_FILE) $(RPM_FILE)
yarn build
</pre>
-### Configuration
+### Build time configuration
You can customize project global variables using env variables. Default values are placed in the `.env` file.
Example:
```
-REACT_APP_ARVADOS_API_HOST=localhost:8000 yarn start
+REACT_APP_ARVADOS_CONFIG_URL=config.json yarn build
+```
+
+### Run time configuration
+The app will fetch runtime configuration when starting. By default it will try to fetch `/config.json`. You can customize this url using build time configuration.
+
+Currently this configuration schema is supported:
+```
+{
+ "API_HOST": "string"
+}
```
### Licensing
--- /dev/null
+{
+ "API_HOST": "CHANGE.TO.YOUR.ARVADOS.API.HOST"
+}
\ No newline at end of file
"version": "0.1.0",
"private": true,
"dependencies": {
- "@material-ui/core": "1.2.1",
+ "@material-ui/core": "1.4.0",
"@material-ui/icons": "1.1.0",
- "@types/lodash": "4.14.109",
+ "@types/lodash": "4.14.112",
"@types/redux-form": "^7.4.1",
"axios": "0.18.0",
- "classnames": "^2.2.6",
+ "classnames": "2.2.6",
"lodash": "4.17.10",
"react": "16.4.1",
"react-dom": "16.4.1",
},
"devDependencies": {
"@types/classnames": "^2.2.4",
- "@types/enzyme": "3.1.10",
+ "@types/enzyme": "3.1.12",
"@types/enzyme-adapter-react-16": "1.0.2",
- "@types/jest": "23.1.0",
- "@types/node": "10.3.3",
- "@types/react": "16.3",
+ "@types/jest": "23.3.0",
+ "@types/node": "10.5.2",
+ "@types/react": "16.4",
"@types/react-dom": "16.0.6",
- "@types/react-redux": "6.0.2",
- "@types/react-router": "4.0.26",
+ "@types/react-redux": "6.0.4",
+ "@types/react-router": "4.0.29",
"@types/react-router-dom": "4.2.7",
"@types/react-router-redux": "5.0.15",
"@types/redux-devtools": "3.0.44",
"@types/redux-form": "^7.4.1",
- "axios-mock-adapter": "^1.15.0",
- "enzyme": "^3.3.0",
- "enzyme-adapter-react-16": "^1.1.1",
+ "axios-mock-adapter": "1.15.0",
+ "enzyme": "3.3.0",
+ "enzyme-adapter-react-16": "1.1.1",
"jest-localstorage-mock": "2.2.0",
"redux-devtools": "3.4.1",
"redux-form": "^7.4.2",
//
// SPDX-License-Identifier: AGPL-3.0
-import CommonResourceService from "./common-resource-service";
+import { CommonResourceService } from "./common-resource-service";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
// SPDX-License-Identifier: AGPL-3.0
import * as _ from "lodash";
-import FilterBuilder from "./filter-builder";
-import OrderBuilder from "./order-builder";
+import { FilterBuilder } from "./filter-builder";
+import { OrderBuilder } from "./order-builder";
import { AxiosInstance, AxiosPromise } from "axios";
import { Resource } from "../../models/resource";
errorToken: string;
}
-export default class CommonResourceService<T extends Resource> {
+export class CommonResourceService<T extends Resource> {
static mapResponseKeys = (response: any): Promise<any> =>
CommonResourceService.mapKeys(_.camelCase)(response.data)
update(uuid: string) {
throw new Error("Not implemented");
}
-
}
//
// SPDX-License-Identifier: AGPL-3.0
-import FilterBuilder from "./filter-builder";
+import { FilterBuilder } from "./filter-builder";
describe("FilterBuilder", () => {
import * as _ from "lodash";
import { Resource } from "../../models/resource";
-export default class FilterBuilder<T extends Resource = Resource> {
-
+export class FilterBuilder<T extends Resource = Resource> {
static create<T extends Resource = Resource>(resourcePrefix = "") {
return new FilterBuilder<T>(resourcePrefix);
}
}
return this;
}
-
}
//
// SPDX-License-Identifier: AGPL-3.0
-import OrderBuilder from "./order-builder";
+import { OrderBuilder } from "./order-builder";
describe("OrderBuilder", () => {
it("should build correct order query", () => {
import * as _ from "lodash";
import { Resource } from "../../models/resource";
-export default class OrderBuilder<T extends Resource = Resource> {
+export class OrderBuilder<T extends Resource = Resource> {
static create<T extends Resource = Resource>(prefix?: string){
return new OrderBuilder<T>([], prefix);
}
private constructor(
- private order: string[] = [],
+ private order: string[] = [],
private prefix = ""){}
private addRule (direction: string, attribute: keyof T) {
export function removeServerApiAuthorizationHeader() {
delete serverApi.defaults.headers.common.Authorization;
}
+
+export const setBaseUrl = (url: string) => {
+ serverApi.defaults.baseURL = url + "/arvados/v1";
+};
//
// SPDX-License-Identifier: AGPL-3.0
-export default class UrlBuilder {
- private url: string = "";
- private query: string = "";
+export class UrlBuilder {
+ private readonly url: string = "";
+ private query: string = "";
- constructor(host: string) {
- this.url = host;
- }
+ constructor(host: string) {
+ this.url = host;
+ }
- public addParam(param: string, value: string) {
- if (this.query.length === 0) {
- this.query += "?";
- } else {
- this.query += "&";
- }
- this.query += `${param}=${value}`;
- return this;
- }
+ public addParam(param: string, value: string) {
+ if (this.query.length === 0) {
+ this.query += "?";
+ } else {
+ this.query += "&";
+ }
+ this.query += `${param}=${value}`;
+ return this;
+ }
- public get() {
- return this.url + this.query;
- }
+ public get() {
+ return this.url + this.query;
+ }
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import Axios from "../../node_modules/axios";
+
+export const CONFIG_URL = process.env.REACT_APP_ARVADOS_CONFIG_URL || "/config.json";
+
+export interface Config {
+ API_HOST: string;
+}
+
+const defaultConfig: Config = {
+ API_HOST: process.env.REACT_APP_ARVADOS_API_HOST || ""
+};
+
+export const fetchConfig = () => {
+ return Axios
+ .get<Config>(CONFIG_URL + "?nocache=" + (new Date()).getTime())
+ .then(response => response.data)
+ .catch(() => Promise.resolve(defaultConfig));
+};
+
import blue from '@material-ui/core/colors/blue';
import grey from '@material-ui/core/colors/grey';
import green from '@material-ui/core/colors/green';
+import yellow from '@material-ui/core/colors/yellow';
+import red from '@material-ui/core/colors/red';
interface ArvadosThemeOptions extends ThemeOptions {
customs: any;
customs: any;
}
-const purple900 = purple["900"];
+const red900 = red["900"];
+const yellow700 = yellow["700"];
+const purple800 = purple["800"];
+const grey200 = grey["200"];
+const grey300 = grey["300"];
+const grey500 = grey["500"];
const grey600 = grey["600"];
+const grey700 = grey["700"];
+const grey900 = grey["900"];
+
const themeOptions: ArvadosThemeOptions = {
customs: {
colors: {
}
},
overrides: {
+ MuiTypography: {
+ body1: {
+ fontSize: '0.8125rem'
+ }
+ },
MuiAppBar: {
colorPrimary: {
- backgroundColor: purple900
+ backgroundColor: purple800
}
},
MuiTabs: {
color: grey600
},
indicator: {
- backgroundColor: purple900
+ backgroundColor: purple800
}
},
MuiTab: {
selected: {
fontWeight: 700,
- color: purple900
+ color: purple800
+ }
+ },
+ MuiList: {
+ root: {
+ color: grey900
+ }
+ },
+ MuiListItemText: {
+ root: {
+ padding: 0
+ }
+ },
+ MuiListItemIcon: {
+ root: {
+ fontSize: '1.25rem'
}
}
},
export const formatFileSize = (size?: number) => {
if (typeof size === "number") {
- for (const { base, unit } of fileSizes) {
+ for (const { base, unit } of FILE_SIZES) {
if (size >= base) {
return `${(size / base).toFixed()} ${unit}`;
}
return "";
};
-const fileSizes = [
+const FILE_SIZES = [
{
base: 1000000000000,
unit: "TB"
--- /dev/null
+export function getUrlParameter(search: string, name: string) {
+ const safeName = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
+ const regex = new RegExp('[\\?&]' + safeName + '=([^&#]*)');
+ const results = regex.exec(search);
+ return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
+}
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import Typography from '@material-ui/core/Typography';
-import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import { ArvadosTheme } from 'src/common/custom-theme';
-
-interface AttributeDataProps {
- label: string;
- value?: string | number;
- link?: string;
-}
-
-type AttributeProps = AttributeDataProps & WithStyles<CssRules>;
-
-class Attribute extends React.Component<AttributeProps> {
-
- hasLink() {
- return !!this.props.link;
- }
-
- render() {
- const { label, link, value, children, classes } = this.props;
- return <Typography component="div" className={classes.attribute}>
- <Typography component="span" className={classes.label}>{label}</Typography>
- { this.hasLink() ? (
- <a href='{link}' className={classes.link} target='_blank'>{value}</a>
- ) : (
- <Typography component="span" className={classes.value}>
- {value}
- {children}
- </Typography>
- )}
- </Typography>;
- }
-
-}
-
-type CssRules = 'attribute' | 'label' | 'value' | 'link';
-
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- attribute: {
- display: 'flex',
- alignItems: 'flex-start',
- marginBottom: theme.spacing.unit
- },
- label: {
- color: theme.palette.grey["500"],
- width: '40%'
- },
- value: {
- width: '60%',
- display: 'flex',
- alignItems: 'flex-start',
- textTransform: 'capitalize'
- },
- link: {
- color: theme.palette.primary.main,
- textDecoration: 'none'
- }
-});
-
-export default withStyles(styles)(Attribute);
\ No newline at end of file
import { mount, configure } from "enzyme";
import * as Adapter from "enzyme-adapter-react-16";
-import Breadcrumbs from "./breadcrumbs";
+import { Breadcrumbs } from "./breadcrumbs";
import { Button } from "@material-ui/core";
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
});
-});
\ No newline at end of file
+});
label: string;
}
+type CssRules = "item" | "currentItem" | "label";
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+ item: {
+ opacity: 0.6
+ },
+ currentItem: {
+ opacity: 1
+ },
+ label: {
+ textTransform: "none"
+ }
+});
+
interface BreadcrumbsProps {
items: Breadcrumb[];
onClick: (breadcrumb: Breadcrumb) => void;
onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: Breadcrumb) => void;
}
-const Breadcrumbs: React.SFC<BreadcrumbsProps & WithStyles<CssRules>> = ({ classes, onClick, onContextMenu, items }) => {
- return <Grid container alignItems="center" wrap="nowrap">
- {
- items.map((item, index) => {
- const isLastItem = index === items.length - 1;
- return (
- <React.Fragment key={index}>
- <Tooltip title={item.label}>
- <Button
+export const Breadcrumbs = withStyles(styles)(
+ ({ classes, onClick, onContextMenu, items }: BreadcrumbsProps & WithStyles<CssRules>) =>
+ <Grid container alignItems="center" wrap="nowrap">
+ {
+ items.map((item, index) => {
+ const isLastItem = index === items.length - 1;
+ return (
+ <React.Fragment key={index}>
+ <Tooltip title={item.label}>
+ <Button
+ color="inherit"
+ className={isLastItem ? classes.currentItem : classes.item}
+ onClick={() => onClick(item)}
+ onContextMenu={event => onContextMenu(event, item)}>
+ <Typography
+ noWrap
color="inherit"
- className={isLastItem ? classes.currentItem : classes.item}
- onClick={() => onClick(item)}
- onContextMenu={event => onContextMenu(event, item)}>
- <Typography
- noWrap
- color="inherit"
- className={classes.label}>
- {item.label}
- </Typography>
- </Button>
- </Tooltip>
- {
- !isLastItem && <ChevronRightIcon color="inherit" className={classes.item} />
- }
- </React.Fragment>
- );
- })
- }
- </Grid>;
-};
-
-type CssRules = "item" | "currentItem" | "label";
-
-const styles: StyleRulesCallback<CssRules> = theme => {
- const { unit } = theme.spacing;
- return {
- item: {
- opacity: 0.6
- },
- currentItem: {
- opacity: 1
- },
- label: {
- textTransform: "none"
- }
- };
-};
-
-export default withStyles(styles)(Breadcrumbs);
-
+ className={classes.label}>
+ {item.label}
+ </Typography>
+ </Button>
+ </Tooltip>
+ {!isLastItem && <ChevronRightIcon color="inherit" className={classes.item} />}
+ </React.Fragment>
+ );
+ })
+ }
+ </Grid>
+);
import * as React from "react";
import { mount, configure } from "enzyme";
import * as Adapter from "enzyme-adapter-react-16";
-import ColumnSelector, { ColumnSelectorProps, ColumnSelectorTrigger } from "./column-selector";
+import { ColumnSelector, ColumnSelectorTrigger } from "./column-selector";
import { ListItem, Checkbox } from "@material-ui/core";
import { DataColumns } from "../data-table/data-table";
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { WithStyles, StyleRulesCallback, Theme, withStyles, IconButton, Paper, List, Checkbox, ListItemText, ListItem } from '@material-ui/core';
+import { WithStyles, StyleRulesCallback, withStyles, IconButton, Paper, List, Checkbox, ListItemText, ListItem } from '@material-ui/core';
import MenuIcon from "@material-ui/icons/Menu";
import { DataColumn, isColumnConfigurable } from '../data-table/data-column';
-import Popover from "../popover/popover";
+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";
-export interface ColumnSelectorProps {
+interface ColumnSelectorDataProps {
columns: DataColumns<any>;
onColumnToggle: (column: DataColumn<any>) => void;
}
-const ColumnSelector: React.SFC<ColumnSelectorProps & WithStyles<CssRules>> = ({ columns, onColumnToggle, classes }) =>
+type CssRules = "checkbox";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ checkbox: {
+ width: 24,
+ height: 24
+ }
+});
+
+export type ColumnSelectorProps = ColumnSelectorDataProps & WithStyles<CssRules>;
+
+export const ColumnSelector = withStyles(styles)(
+ ({ columns, onColumnToggle, classes }: ColumnSelectorProps) =>
<Popover triggerComponent={ColumnSelectorTrigger}>
<Paper>
<List dense>
))}
</List>
</Paper>
- </Popover>;
+ </Popover>
+);
-export const ColumnSelectorTrigger: React.SFC<IconButtonProps> = (props) =>
+export const ColumnSelectorTrigger = (props: IconButtonProps) =>
<IconButton {...props}>
<MenuIcon />
</IconButton>;
-
-type CssRules = "checkbox";
-
-const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
- checkbox: {
- width: 24,
- height: 24
- }
-});
-
-export default withStyles(styles)(ColumnSelector);
import * as React from "react";
import { mount, configure, shallow } from "enzyme";
import * as Adapter from "enzyme-adapter-react-16";
-import ContextMenu from "./context-menu";
+import { ContextMenu } from "./context-menu";
import { ListItem } from "@material-ui/core";
+import { ShareIcon } from "../icon/icon";
configure({ adapter: new Adapter() });
describe("<ContextMenu />", () => {
- const actions = [[{
- icon: "",
+ const items = [[{
+ icon: ShareIcon,
name: "Action 1.1"
}, {
- icon: "",
+ icon: ShareIcon,
name: "Action 1.2"
},], [{
- icon: "",
+ icon: ShareIcon,
name: "Action 2.1"
}]];
- it("calls onActionClick with clicked action", () => {
- const onActionClick = jest.fn();
+ it("calls onItemClick with clicked action", () => {
+ const onItemClick = jest.fn();
const contextMenu = mount(<ContextMenu
anchorEl={document.createElement("div")}
onClose={jest.fn()}
- onActionClick={onActionClick}
- actions={actions} />);
+ onItemClick={onItemClick}
+ items={items} />);
contextMenu.find(ListItem).at(2).simulate("click");
- expect(onActionClick).toHaveBeenCalledWith(actions[1][0]);
+ expect(onItemClick).toHaveBeenCalledWith(items[1][0]);
});
-});
\ No newline at end of file
+});
import * as React from "react";
import { Popover, List, ListItem, ListItemIcon, ListItemText, Divider } from "@material-ui/core";
import { DefaultTransformOrigin } from "../popover/helpers";
+import { IconType } from "../icon/icon";
-export interface ContextMenuAction {
+export interface ContextMenuItem {
name: string;
- icon: string;
- openCreateDialog?: boolean;
+ icon: IconType;
}
-export type ContextMenuActionGroup = ContextMenuAction[];
+export type ContextMenuItemGroup = ContextMenuItem[];
-export interface ContextMenuProps<T> {
+export interface ContextMenuProps {
anchorEl?: HTMLElement;
- actions: ContextMenuActionGroup[];
- onActionClick: (action: ContextMenuAction) => void;
+ items: ContextMenuItemGroup[];
+ onItemClick: (action: ContextMenuItem) => void;
onClose: () => void;
}
-export default class ContextMenu<T> extends React.PureComponent<ContextMenuProps<T>> {
+export class ContextMenu extends React.PureComponent<ContextMenuProps> {
render() {
- const { anchorEl, actions, onClose, onActionClick } = this.props;
+ const { anchorEl, items, onClose, onItemClick} = this.props;
return <Popover
anchorEl={anchorEl}
open={!!anchorEl}
anchorOrigin={DefaultTransformOrigin}
onContextMenu={this.handleContextMenu}>
<List dense>
- {actions.map((group, groupIndex) =>
+ {items.map((group, groupIndex) =>
<React.Fragment key={groupIndex}>
- {group.map((action, actionIndex) =>
+ {group.map((item, actionIndex) =>
<ListItem
button
key={actionIndex}
- onClick={() => onActionClick(action)}>
+ onClick={() => onItemClick(item)}>
<ListItemIcon>
- <i className={action.icon} />
+ <item.icon/>
</ListItemIcon>
<ListItemText>
- {action.name}
+ {item.name}
</ListItemText>
</ListItem>)}
- {groupIndex < actions.length - 1 && <Divider />}
+ {groupIndex < items.length - 1 && <Divider />}
</React.Fragment>)}
</List>
</Popover>;
import { configure, mount } from "enzyme";
import * as Adapter from 'enzyme-adapter-react-16';
-import DataExplorer from "./data-explorer";
-import ColumnSelector from "../column-selector/column-selector";
-import DataTable from "../data-table/data-table";
-import SearchInput from "../search-input/search-input";
+import { DataExplorer } from "./data-explorer";
+import { ColumnSelector } from "../column-selector/column-selector";
+import { DataTable } from "../data-table/data-table";
+import { SearchInput } from "../search-input/search-input";
import { TablePagination } from "@material-ui/core";
configure({ adapter: new Adapter() });
onChangePage: jest.fn(),
onChangeRowsPerPage: jest.fn(),
onContextMenu: jest.fn()
-});
\ No newline at end of file
+});
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, Theme, WithStyles, TablePagination, IconButton } from '@material-ui/core';
+import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, WithStyles, TablePagination, IconButton } from '@material-ui/core';
import MoreVertIcon from "@material-ui/icons/MoreVert";
-import ColumnSelector from "../../components/column-selector/column-selector";
-import DataTable, { DataColumns } from "../../components/data-table/data-table";
-import { DataColumn } from "../../components/data-table/data-column";
-import { DataTableFilterItem } from '../../components/data-table-filters/data-table-filters';
-import SearchInput from '../search-input/search-input';
+import { ColumnSelector } from "../column-selector/column-selector";
+import { DataTable, DataColumns } from "../data-table/data-table";
+import { DataColumn } 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";
-interface DataExplorerProps<T> {
+type CssRules = "searchBox" | "toolbar";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ searchBox: {
+ paddingBottom: theme.spacing.unit * 2
+ },
+ toolbar: {
+ paddingTop: theme.spacing.unit * 2
+ }
+});
+
+interface DataExplorerDataProps<T> {
items: T[];
itemsAvailable: number;
columns: DataColumns<T>;
extractKey?: (item: T) => React.Key;
}
-class DataExplorer<T> extends React.Component<DataExplorerProps<T> & WithStyles<CssRules>> {
+type DataExplorerProps<T> = DataExplorerDataProps<T> & WithStyles<CssRules>;
- render() {
- return <Paper>
- <Toolbar className={this.props.classes.toolbar}>
- <Grid container justify="space-between" wrap="nowrap" alignItems="center">
- <div className={this.props.classes.searchBox}>
- <SearchInput
- value={this.props.searchValue}
- onSearch={this.props.onSearch} />
- </div>
- <ColumnSelector
- columns={this.props.columns}
- onColumnToggle={this.props.onColumnToggle} />
- </Grid>
- </Toolbar>
- <DataTable
- columns={[...this.props.columns, this.contextMenuColumn]}
- items={this.props.items}
- onRowClick={(_, item: T) => this.props.onRowClick(item)}
- onContextMenu={this.props.onContextMenu}
- onRowDoubleClick={(_, item: T) => this.props.onRowDoubleClick(item)}
- onFiltersChange={this.props.onFiltersChange}
- onSortToggle={this.props.onSortToggle}
- extractKey={this.props.extractKey} />
- <Toolbar>
- {this.props.items.length > 0 &&
+export const DataExplorer = withStyles(styles)(
+ class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
+ render() {
+ return <Paper>
+ <Toolbar className={this.props.classes.toolbar}>
+ <Grid container justify="space-between" wrap="nowrap" alignItems="center">
+ <div className={this.props.classes.searchBox}>
+ <SearchInput
+ value={this.props.searchValue}
+ onSearch={this.props.onSearch}/>
+ </div>
+ <ColumnSelector
+ columns={this.props.columns}
+ onColumnToggle={this.props.onColumnToggle}/>
+ </Grid>
+ </Toolbar>
+ <DataTable
+ columns={[...this.props.columns, this.contextMenuColumn]}
+ items={this.props.items}
+ onRowClick={(_, item: T) => this.props.onRowClick(item)}
+ onContextMenu={this.props.onContextMenu}
+ onRowDoubleClick={(_, item: T) => this.props.onRowDoubleClick(item)}
+ onFiltersChange={this.props.onFiltersChange}
+ onSortToggle={this.props.onSortToggle}
+ extractKey={this.props.extractKey}/>
+ <Toolbar>
+ {this.props.items.length > 0 &&
<Grid container justify="flex-end">
<TablePagination
count={this.props.itemsAvailable}
component="div"
/>
</Grid>}
- </Toolbar>
- </Paper>;
- }
+ </Toolbar>
+ </Paper>;
+ }
- changePage = (event: React.MouseEvent<HTMLButtonElement> | null, page: number) => {
- this.props.onChangePage(page);
- }
+ changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
+ this.props.onChangePage(page);
+ }
- changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
- this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
- }
+ changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
+ this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
+ }
- renderContextMenuTrigger = (item: T) =>
- <Grid container justify="flex-end">
- <IconButton onClick={event => this.props.onContextMenu(event, item)}>
- <MoreVertIcon />
- </IconButton>
- </Grid>
+ renderContextMenuTrigger = (item: T) =>
+ <Grid container justify="flex-end">
+ <IconButton onClick={event => this.props.onContextMenu(event, item)}>
+ <MoreVertIcon/>
+ </IconButton>
+ </Grid>
- contextMenuColumn = {
- name: "Actions",
- selected: true,
- key: "context-actions",
- renderHeader: () => null,
- render: this.renderContextMenuTrigger,
- width: "auto"
- };
-
-}
-
-type CssRules = "searchBox" | "toolbar";
-
-const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
- searchBox: {
- paddingBottom: theme.spacing.unit * 2
- },
- toolbar: {
- paddingTop: theme.spacing.unit * 2
+ contextMenuColumn = {
+ name: "Actions",
+ selected: true,
+ key: "context-actions",
+ renderHeader: () => null,
+ render: this.renderContextMenuTrigger,
+ width: "auto"
+ };
}
-});
-
-export default withStyles(styles)(DataExplorer);
+);
import * as React from "react";
import { mount, configure } from "enzyme";
-import DataTableFilter, { DataTableFilterItem } from "./data-table-filters";
+import { DataTableFilters, DataTableFilterItem } from "./data-table-filters";
import * as Adapter from 'enzyme-adapter-react-16';
import { Checkbox, ButtonBase, ListItem, Button, ListItemText } from "@material-ui/core";
name: "Filter 2",
selected: false
}];
- const dataTableFilter = mount(<DataTableFilter name="" filters={filters} />);
+ const dataTableFilter = mount(<DataTableFilters name="" filters={filters} />);
dataTableFilter.find(ButtonBase).simulate("click");
expect(dataTableFilter.find(Checkbox).at(0).prop("checked")).toBeTruthy();
expect(dataTableFilter.find(Checkbox).at(1).prop("checked")).toBeFalsy();
});
-
+
it("updates filters after filters prop change", () => {
const filters = [{
name: "Filter 1",
name: "Filter 2",
selected: true
}];
- const dataTableFilter = mount(<DataTableFilter name="" filters={filters} />);
+ const dataTableFilter = mount(<DataTableFilters name="" filters={filters} />);
dataTableFilter.find(ButtonBase).simulate("click");
expect(dataTableFilter.find(Checkbox).prop("checked")).toBeTruthy();
dataTableFilter.find(ListItem).simulate("click");
selected: false
}];
const onChange = jest.fn();
- const dataTableFilter = mount(<DataTableFilter name="" filters={filters} onChange={onChange} />);
+ const dataTableFilter = mount(<DataTableFilters name="" filters={filters} onChange={onChange} />);
dataTableFilter.find(ButtonBase).simulate("click");
dataTableFilter.find(ListItem).at(1).simulate("click");
dataTableFilter.find(Button).at(0).simulate("click");
selected: true
}]);
});
-});
\ No newline at end of file
+});
import * as classnames from "classnames";
import { DefaultTransformOrigin } from "../popover/helpers";
-export interface DataTableFilterItem {
- name: string;
- selected: boolean;
-}
-
-export interface DataTableFilterProps {
- name: string;
- filters: DataTableFilterItem[];
- onChange?: (filters: DataTableFilterItem[]) => void;
-}
-
-interface DataTableFilterState {
- anchorEl?: HTMLElement;
- filters: DataTableFilterItem[];
- prevFilters: DataTableFilterItem[];
-}
-
-class DataTableFilter extends React.Component<DataTableFilterProps & WithStyles<CssRules>, DataTableFilterState> {
- state: DataTableFilterState = {
- anchorEl: undefined,
- filters: [],
- prevFilters: []
- };
- icon = React.createRef<HTMLElement>();
-
- render() {
- const { name, classes, children } = this.props;
- const isActive = this.state.filters.some(f => f.selected);
- return <>
- <ButtonBase
- className={classnames([classes.root, { [classes.active]: isActive }])}
- component="span"
- onClick={this.open}
- disableRipple>
- {children}
- <i className={classnames(["fas fa-filter", classes.icon])}
- data-fa-transform="shrink-3"
- ref={this.icon} />
- </ButtonBase>
- <Popover
- anchorEl={this.state.anchorEl}
- open={!!this.state.anchorEl}
- anchorOrigin={DefaultTransformOrigin}
- transformOrigin={DefaultTransformOrigin}
- onClose={this.cancel}>
- <Card>
- <CardContent>
- <Typography variant="caption">
- {name}
- </Typography>
- </CardContent>
- <List dense>
- {this.state.filters.map((filter, index) =>
- <ListItem
- button
- key={index}
- onClick={this.toggleFilter(filter)}>
- <Checkbox
- disableRipple
- color="primary"
- checked={filter.selected}
- className={classes.checkbox} />
- <ListItemText>
- {filter.name}
- </ListItemText>
- </ListItem>
- )}
- </List>
- <CardActions>
- <Button
- color="primary"
- variant="raised"
- size="small"
- onClick={this.submit}>
- Ok
- </Button>
- <Button
- color="primary"
- variant="outlined"
- size="small"
- onClick={this.cancel}>
- Cancel
- </Button>
- </CardActions >
- </Card>
- </Popover>
- </>;
- }
-
- static getDerivedStateFromProps(props: DataTableFilterProps, state: DataTableFilterState): DataTableFilterState {
- return props.filters !== state.prevFilters
- ? { ...state, filters: props.filters, prevFilters: props.filters }
- : state;
- }
-
- open = () => {
- this.setState({ anchorEl: this.icon.current || undefined });
- }
-
- submit = () => {
- const { onChange } = this.props;
- if (onChange) {
- onChange(this.state.filters);
- }
- this.setState({ anchorEl: undefined });
- }
-
- cancel = () => {
- this.setState(prev => ({
- ...prev,
- filters: prev.prevFilters,
- anchorEl: undefined
- }));
- }
-
- toggleFilter = (toggledFilter: DataTableFilterItem) => () => {
- this.setState(prev => ({
- ...prev,
- filters: prev.filters.map(filter =>
- filter === toggledFilter
- ? { ...filter, selected: !filter.selected }
- : filter)
- }));
- }
-}
-
-
export type CssRules = "root" | "icon" | "active" | "checkbox";
const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
}
});
-export default withStyles(styles)(DataTableFilter);
+export interface DataTableFilterItem {
+ name: string;
+ selected: boolean;
+}
+
+export interface DataTableFilterProps {
+ name: string;
+ filters: DataTableFilterItem[];
+ onChange?: (filters: DataTableFilterItem[]) => void;
+}
+
+interface DataTableFilterState {
+ anchorEl?: HTMLElement;
+ filters: DataTableFilterItem[];
+ prevFilters: DataTableFilterItem[];
+}
+
+export const DataTableFilters = withStyles(styles)(
+ class extends React.Component<DataTableFilterProps & WithStyles<CssRules>, DataTableFilterState> {
+ state: DataTableFilterState = {
+ anchorEl: undefined,
+ filters: [],
+ prevFilters: []
+ };
+ icon = React.createRef<HTMLElement>();
+
+ render() {
+ const { name, classes, children } = this.props;
+ const isActive = this.state.filters.some(f => f.selected);
+ return <>
+ <ButtonBase
+ className={classnames([classes.root, { [classes.active]: isActive }])}
+ component="span"
+ onClick={this.open}
+ disableRipple>
+ {children}
+ <i className={classnames(["fas fa-filter", classes.icon])}
+ data-fa-transform="shrink-3"
+ ref={this.icon} />
+ </ButtonBase>
+ <Popover
+ anchorEl={this.state.anchorEl}
+ open={!!this.state.anchorEl}
+ anchorOrigin={DefaultTransformOrigin}
+ transformOrigin={DefaultTransformOrigin}
+ onClose={this.cancel}>
+ <Card>
+ <CardContent>
+ <Typography variant="caption">
+ {name}
+ </Typography>
+ </CardContent>
+ <List dense>
+ {this.state.filters.map((filter, index) =>
+ <ListItem
+ button
+ key={index}
+ onClick={this.toggleFilter(filter)}>
+ <Checkbox
+ disableRipple
+ color="primary"
+ checked={filter.selected}
+ className={classes.checkbox} />
+ <ListItemText>
+ {filter.name}
+ </ListItemText>
+ </ListItem>
+ )}
+ </List>
+ <CardActions>
+ <Button
+ color="primary"
+ variant="raised"
+ size="small"
+ onClick={this.submit}>
+ Ok
+ </Button>
+ <Button
+ color="primary"
+ variant="outlined"
+ size="small"
+ onClick={this.cancel}>
+ Cancel
+ </Button>
+ </CardActions >
+ </Card>
+ </Popover>
+ </>;
+ }
+
+ static getDerivedStateFromProps(props: DataTableFilterProps, state: DataTableFilterState): DataTableFilterState {
+ return props.filters !== state.prevFilters
+ ? { ...state, filters: props.filters, prevFilters: props.filters }
+ : state;
+ }
+
+ open = () => {
+ this.setState({ anchorEl: this.icon.current || undefined });
+ }
+
+ submit = () => {
+ const { onChange } = this.props;
+ if (onChange) {
+ onChange(this.state.filters);
+ }
+ this.setState({ anchorEl: undefined });
+ }
+
+ cancel = () => {
+ this.setState(prev => ({
+ ...prev,
+ filters: prev.prevFilters,
+ anchorEl: undefined
+ }));
+ }
+
+ toggleFilter = (toggledFilter: DataTableFilterItem) => () => {
+ this.setState(prev => ({
+ ...prev,
+ filters: prev.filters.map(filter =>
+ filter === toggledFilter
+ ? { ...filter, selected: !filter.selected }
+ : filter)
+ }));
+ }
+ }
+);
import { mount, configure } from "enzyme";
import { TableHead, TableCell, Typography, TableBody, Button, TableSortLabel } from "@material-ui/core";
import * as Adapter from "enzyme-adapter-react-16";
-import DataTable, { DataColumns } from "./data-table";
-import DataTableFilters from "../data-table-filters/data-table-filters";
+import { DataTable, DataColumns } from "./data-table";
+import { DataTableFilters } from "../data-table-filters/data-table-filters";
import { SortDirection } from "./data-column";
configure({ adapter: new Adapter() });
dataTable.find(DataTableFilters).prop("onChange")([]);
expect(onFiltersChange).toHaveBeenCalledWith([], columns[0]);
});
-
-
-});
\ No newline at end of file
+});
import * as React from 'react';
import { Table, TableBody, TableRow, TableCell, TableHead, TableSortLabel, StyleRulesCallback, Theme, WithStyles, withStyles, Typography } from '@material-ui/core';
import { DataColumn, SortDirection } from './data-column';
-import DataTableFilters, { DataTableFilterItem } from "../data-table-filters/data-table-filters";
+import { DataTableFilters, DataTableFilterItem } from "../data-table-filters/data-table-filters";
export type DataColumns<T, F extends DataTableFilterItem = DataTableFilterItem> = Array<DataColumn<T, F>>;
-export interface DataTableProps<T> {
+export interface DataTableDataProps<T> {
items: T[];
columns: DataColumns<T>;
onRowClick: (event: React.MouseEvent<HTMLTableRowElement>, item: T) => void;
extractKey?: (item: T) => React.Key;
}
-class DataTable<T> extends React.Component<DataTableProps<T> & WithStyles<CssRules>> {
- render() {
- const { items, classes } = this.props;
- return <div
- className={classes.tableContainer}>
- <Table>
- <TableHead>
- <TableRow>
- {this.mapVisibleColumns(this.renderHeadCell)}
- </TableRow>
- </TableHead>
- <TableBody className={classes.tableBody}>
- {items.map(this.renderBodyRow)}
- </TableBody>
- </Table>
- </div>;
- }
-
- renderHeadCell = (column: DataColumn<T>, index: number) => {
- const { name, key, renderHeader, filters, sortDirection } = column;
- const { onSortToggle, onFiltersChange } = this.props;
- return <TableCell key={key || index} style={{ width: column.width, minWidth: column.width }}>
- {renderHeader ?
- renderHeader() :
- filters
- ? <DataTableFilters
- name={`${name} filters`}
- onChange={filters =>
- onFiltersChange &&
- onFiltersChange(filters, column)}
- filters={filters}>
- {name}
- </DataTableFilters>
- : sortDirection
- ? <TableSortLabel
- active={sortDirection !== SortDirection.None}
- direction={sortDirection !== SortDirection.None ? sortDirection : undefined}
- onClick={() =>
- onSortToggle &&
- onSortToggle(column)}>
- {name}
- </TableSortLabel>
- : <span>
- {name}
- </span>}
- </TableCell>;
- }
-
- renderBodyRow = (item: T, index: number) => {
- const { onRowClick, onRowDoubleClick, extractKey } = this.props;
- return <TableRow
- hover
- key={extractKey ? extractKey(item) : index}
- onClick={event => onRowClick && onRowClick(event, item)}
- onContextMenu={this.handleRowContextMenu(item)}
- onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item) }>
- {this.mapVisibleColumns((column, index) => (
- <TableCell key={column.key || index}>
- {column.render(item)}
- </TableCell>
- ))}
- </TableRow>;
- }
-
- mapVisibleColumns = (fn: (column: DataColumn<T>, index: number) => React.ReactElement<any>) => {
- return this.props.columns.filter(column => column.selected).map(fn);
- }
-
- handleRowContextMenu = (item: T) =>
- (event: React.MouseEvent<HTMLElement>) =>
- this.props.onContextMenu(event, item)
-
-}
-
type CssRules = "tableBody" | "tableContainer" | "noItemsInfo";
const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
}
});
-export default withStyles(styles)(DataTable);
+type DataTableProps<T> = DataTableDataProps<T> & WithStyles<CssRules>;
+
+export const DataTable = withStyles(styles)(
+ class Component<T> extends React.Component<DataTableProps<T>> {
+ render() {
+ const { items, classes } = this.props;
+ return <div
+ className={classes.tableContainer}>
+ <Table>
+ <TableHead>
+ <TableRow>
+ {this.mapVisibleColumns(this.renderHeadCell)}
+ </TableRow>
+ </TableHead>
+ <TableBody className={classes.tableBody}>
+ {items.map(this.renderBodyRow)}
+ </TableBody>
+ </Table>
+ </div>;
+ }
+
+ renderHeadCell = (column: DataColumn<T>, index: number) => {
+ const { name, key, renderHeader, filters, sortDirection } = column;
+ const { onSortToggle, onFiltersChange } = this.props;
+ return <TableCell key={key || index} style={{ width: column.width, minWidth: column.width }}>
+ {renderHeader ?
+ renderHeader() :
+ filters
+ ? <DataTableFilters
+ name={`${name} filters`}
+ onChange={filters =>
+ onFiltersChange &&
+ onFiltersChange(filters, column)}
+ filters={filters}>
+ {name}
+ </DataTableFilters>
+ : sortDirection
+ ? <TableSortLabel
+ active={sortDirection !== SortDirection.None}
+ direction={sortDirection !== SortDirection.None ? sortDirection : undefined}
+ onClick={() =>
+ onSortToggle &&
+ onSortToggle(column)}>
+ {name}
+ </TableSortLabel>
+ : <span>
+ {name}
+ </span>}
+ </TableCell>;
+ }
+
+ renderBodyRow = (item: T, index: number) => {
+ const { onRowClick, onRowDoubleClick, extractKey } = this.props;
+ return <TableRow
+ hover
+ key={extractKey ? extractKey(item) : index}
+ onClick={event => onRowClick && onRowClick(event, item)}
+ onContextMenu={this.handleRowContextMenu(item)}
+ onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item) }>
+ {this.mapVisibleColumns((column, index) => (
+ <TableCell key={column.key || index}>
+ {column.render(item)}
+ </TableCell>
+ ))}
+ </TableRow>;
+ }
+
+ mapVisibleColumns = (fn: (column: DataColumn<T>, index: number) => React.ReactElement<any>) => {
+ return this.props.columns.filter(column => column.selected).map(fn);
+ }
+
+ handleRowContextMenu = (item: T) =>
+ (event: React.MouseEvent<HTMLElement>) =>
+ this.props.onContextMenu(event, item)
+
+ }
+);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+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';
+
+type CssRules = 'attribute' | 'label' | 'value' | 'link';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ attribute: {
+ display: 'flex',
+ alignItems: 'flex-start',
+ marginBottom: theme.spacing.unit
+ },
+ label: {
+ color: theme.palette.grey["500"],
+ width: '40%'
+ },
+ value: {
+ width: '60%',
+ display: 'flex',
+ alignItems: 'flex-start',
+ textTransform: 'capitalize'
+ },
+ link: {
+ width: '60%',
+ color: theme.palette.primary.main,
+ textDecoration: 'none',
+ overflowWrap: 'break-word'
+ }
+});
+
+interface DetailsAttributeDataProps {
+ label: string;
+ value?: string | number;
+ link?: string;
+ children?: React.ReactNode;
+}
+
+type DetailsAttributeProps = DetailsAttributeDataProps & WithStyles<CssRules>;
+
+export const DetailsAttribute = withStyles(styles)(({ label, link, value, children, classes }: DetailsAttributeProps) =>
+ <Typography component="div" className={classes.attribute}>
+ <Typography component="span" className={classes.label}>{label}</Typography>
+ { link
+ ? <a href={link} className={classes.link} target='_blank'>{value}</a>
+ : <Typography component="span" className={classes.value}>
+ {value}
+ {children}
+ </Typography> }
+ </Typography>
+);
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import ProjectItem from './items/project-item';
-import CollectionItem from './items/collection-item';
-import ProcessItem from './items/process-item';
-import AbstractItem from './items/abstract-item';
-import EmptyItem from './items/empty-item';
-import { DetailsPanelResource } from '../../views-components/details-panel/details-panel';
-import { EmptyResource } from '../../models/empty';
-import { ResourceKind } from '../../models/resource';
-
-export default class DetailsPanelFactory {
- static createItem(res: DetailsPanelResource): AbstractItem {
- switch (res.kind) {
- case ResourceKind.Project:
- return new ProjectItem(res);
- case ResourceKind.Collection:
- return new CollectionItem(res);
- case ResourceKind.Process:
- return new ProcessItem(res);
- default:
- return new EmptyItem(res as EmptyResource);
- }
- }
-}
\ No newline at end of file
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import { IconTypes } from '../../icon/icon';
-import { DetailsPanelResource } from '../../../views-components/details-panel/details-panel';
-
-export default abstract class AbstractItem<T extends DetailsPanelResource = DetailsPanelResource> {
-
- constructor(protected item: T) {}
-
- getTitle(): string {
- return this.item.name;
- }
-
- abstract getIcon(): IconTypes;
- abstract buildDetails(): React.ReactElement<any>;
-
- buildActivity(): React.ReactElement<any> {
- return <div/>;
- }
-}
\ No newline at end of file
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import { IconTypes } from '../../icon/icon';
-import Attribute from '../../attribute/attribute';
-import AbstractItem from './abstract-item';
-import { CollectionResource } from '../../../models/collection';
-import { formatDate } from '../../../common/formatters';
-
-export default class CollectionItem extends AbstractItem<CollectionResource> {
-
- getIcon(): IconTypes {
- return IconTypes.COLLECTION;
- }
-
- buildDetails(): React.ReactElement<any> {
- return <div>
- <Attribute label='Type' value='Data Collection' />
- <Attribute label='Size' value='---' />
- <Attribute label='Owner' value={this.item.ownerUuid} />
- <Attribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
- <Attribute label='Created at' value={formatDate(this.item.createdAt)} />
- {/* Links but we dont have view */}
- <Attribute label='Collection UUID' link={this.item.uuid} value={this.item.uuid} />
- <Attribute label='Content address' link={this.item.portableDataHash} value={this.item.portableDataHash} />
- {/* Missing attrs */}
- <Attribute label='Number of files' value='20' />
- <Attribute label='Content size' value='54 MB' />
- </div>;
- }
-}
\ No newline at end of file
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import { IconTypes } from '../../icon/icon';
-import AbstractItem from './abstract-item';
-import EmptyState from '../../empty-state/empty-state';
-import { EmptyResource } from '../../../models/empty';
-
-export default class EmptyItem extends AbstractItem<EmptyResource> {
-
- getIcon(): IconTypes {
- return IconTypes.FOLDER;
- }
-
- buildDetails(): React.ReactElement<any> {
- return <EmptyState icon={IconTypes.ANNOUNCEMENT}
- message='Select a file or folder to view its details.' />;
- }
-}
\ No newline at end of file
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import IconBase, { IconTypes } from '../../icon/icon';
-import Attribute from '../../attribute/attribute';
-import AbstractItem from './abstract-item';
-import { ProcessResource } from '../../../models/process';
-import { formatDate } from '../../../common/formatters';
-
-export default class ProcessItem extends AbstractItem<ProcessResource> {
-
- getIcon(): IconTypes {
- return IconTypes.PROCESS;
- }
-
- buildDetails(): React.ReactElement<any> {
- return <div>
- <Attribute label='Type' value='Process' />
- <Attribute label='Size' value='---' />
- <Attribute label='Owner' value={this.item.ownerUuid} />
-
- {/* Missing attr */}
- <Attribute label='Status' value={this.item.state} />
- <Attribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
-
- {/* Missing attrs */}
- <Attribute label='Started at' value={formatDate(this.item.createdAt)} />
- <Attribute label='Finished at' value={formatDate(this.item.expiresAt)} />
-
- {/* Links but we dont have view */}
- <Attribute label='Outputs' link={this.item.outputPath} value={this.item.outputPath} />
- <Attribute label='UUID' link={this.item.uuid} value={this.item.uuid} />
- <Attribute label='Container UUID' link={this.item.containerUuid} value={this.item.containerUuid} />
-
- <Attribute label='Priority' value={this.item.priority} />
- <Attribute label='Runtime Constraints' value={this.item.runtimeConstraints} />
- {/* Link but we dont have view */}
- <Attribute label='Docker Image locator' link={this.item.containerImage} value={this.item.containerImage} />
- </div>;
- }
-}
\ No newline at end of file
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import { IconTypes } from '../../icon/icon';
-import Attribute from '../../attribute/attribute';
-import AbstractItem from './abstract-item';
-import { ProjectResource } from '../../../models/project';
-import { formatDate } from '../../../common/formatters';
-
-export default class ProjectItem extends AbstractItem<ProjectResource> {
-
- getIcon(): IconTypes {
- return IconTypes.FOLDER;
- }
-
- buildDetails(): React.ReactElement<any> {
- return <div>
- <Attribute label='Type' value={this.item.groupClass} />
- {/* Missing attr */}
- <Attribute label='Size' value='---' />
- <Attribute label='Owner' value={this.item.ownerUuid} />
- <Attribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
- <Attribute label='Created at' value={formatDate(this.item.createdAt)} />
- {/* Missing attr */}
- <Attribute label='File size' value='1.4 GB' />
- <Attribute label='Description' value={this.item.description} />
- </div>;
- }
-}
\ No newline at end of file
import * as React from "react";
import { shallow, configure } from "enzyme";
-import DropdownMenu from "./dropdown-menu";
-import ChevronRightIcon from '@material-ui/icons/ChevronRight';
-
+import { DropdownMenu } from "./dropdown-menu";
import * as Adapter from 'enzyme-adapter-react-16';
import { MenuItem, IconButton, Menu } from "@material-ui/core";
+import { PaginationRightArrowIcon } from "../icon/icon";
configure({ adapter: new Adapter() });
describe("<DropdownMenu />", () => {
it("renders menu icon", () => {
- const dropdownMenu = shallow(<DropdownMenu id="test-menu" icon={ChevronRightIcon} />);
- expect(dropdownMenu.find(ChevronRightIcon)).toHaveLength(1);
+ const dropdownMenu = shallow(<DropdownMenu id="test-menu" icon={<PaginationRightArrowIcon />} />);
+ expect(dropdownMenu.find(PaginationRightArrowIcon)).toHaveLength(1);
});
it("render menu items", () => {
const dropdownMenu = shallow(
- <DropdownMenu id="test-menu" icon={ChevronRightIcon}>
+ <DropdownMenu id="test-menu" icon={<PaginationRightArrowIcon />}>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
</DropdownMenu>
});
it("opens on menu icon click", () => {
- const dropdownMenu = shallow(<DropdownMenu id="test-menu" icon={ChevronRightIcon} />);
+ const dropdownMenu = shallow(<DropdownMenu id="test-menu" icon={<PaginationRightArrowIcon />} />);
dropdownMenu.find(IconButton).simulate("click", {currentTarget: {}});
expect(dropdownMenu.state().anchorEl).toBeDefined();
});
-
+
it("closes on menu click", () => {
- const dropdownMenu = shallow(<DropdownMenu id="test-menu" icon={ChevronRightIcon} />);
+ const dropdownMenu = shallow(<DropdownMenu id="test-menu" icon={<PaginationRightArrowIcon />} />);
dropdownMenu.find(Menu).simulate("click", {currentTarget: {}});
expect(dropdownMenu.state().anchorEl).toBeUndefined();
});
-});
\ No newline at end of file
+});
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { Menu, IconButton } from '@material-ui/core';
+import Menu from '@material-ui/core/Menu';
+import IconButton from '@material-ui/core/IconButton';
import { PopoverOrigin } from '@material-ui/core/Popover';
-
interface DropdownMenuProps {
id: string;
- icon: React.ComponentType;
+ icon: React.ReactElement<any>;
}
-class DropdownMenu extends React.Component<DropdownMenuProps> {
+interface DropdownMenuState {
+ anchorEl: any;
+}
+export class DropdownMenu extends React.Component<DropdownMenuProps, DropdownMenuState> {
state = {
anchorEl: undefined
};
};
render() {
- const { icon: Icon, id, children } = this.props;
+ const { icon, id, children } = this.props;
const { anchorEl } = this.state;
return (
<div>
aria-owns={anchorEl ? id : undefined}
aria-haspopup="true"
color="inherit"
- onClick={this.handleOpen}
-
- >
- <Icon />
+ onClick={this.handleOpen}>
+ {icon}
</IconButton>
<Menu
id={id}
onClose={this.handleClose}
onClick={this.handleClose}
anchorOrigin={this.transformOrigin}
- transformOrigin={this.transformOrigin}
- >
+ transformOrigin={this.transformOrigin}>
{children}
</Menu>
</div>
this.setState({ anchorEl: event.currentTarget });
}
}
-
-
-export default DropdownMenu;
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import Typography from '@material-ui/core/Typography';
-import { WithStyles, withStyles, StyleRulesCallback } from '@material-ui/core/styles';
-import { ArvadosTheme } from 'src/common/custom-theme';
-import IconBase, { IconTypes } from '../icon/icon';
-
-export interface EmptyStateDataProps {
- message: string;
- icon: IconTypes;
- details?: string;
-}
-
-type EmptyStateProps = EmptyStateDataProps & WithStyles<CssRules>;
-
-class EmptyState extends React.Component<EmptyStateProps, {}> {
-
- render() {
- const { classes, message, details, icon, children } = this.props;
- return (
- <Typography className={classes.container} component="div">
- <IconBase icon={icon} className={classes.icon} />
- <Typography variant="body1" gutterBottom>{message}</Typography>
- { details && <Typography gutterBottom>{details}</Typography> }
- { children && <Typography gutterBottom>{children}</Typography> }
- </Typography>
- );
- }
-
-}
-
-type CssRules = 'container' | 'icon';
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- container: {
- textAlign: 'center'
- },
- icon: {
- color: theme.palette.grey["500"],
- fontSize: '72px'
- }
-});
-
-export default withStyles(styles)(EmptyState);
\ No newline at end of file
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import * as classnames from "classnames";
-import CloseAnnouncement from '@material-ui/icons/Announcement';
-import CloseIcon from '@material-ui/icons/Close';
-import FolderIcon from '@material-ui/icons/Folder';
+import AccessTime from '@material-ui/icons/AccessTime';
+import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
+import BubbleChart from '@material-ui/icons/BubbleChart';
+import Cached from '@material-ui/icons/Cached';
+import Code from '@material-ui/icons/Code';
+import ChevronLeft from '@material-ui/icons/ChevronLeft';
+import ChevronRight from '@material-ui/icons/ChevronRight';
+import Close from '@material-ui/icons/Close';
+import ContentCopy from '@material-ui/icons/ContentCopy';
+import CreateNewFolder from '@material-ui/icons/CreateNewFolder';
+import Delete from '@material-ui/icons/Delete';
+import Edit from '@material-ui/icons/Edit';
+import Folder from '@material-ui/icons/Folder';
+import GetApp from '@material-ui/icons/GetApp';
+import Help from '@material-ui/icons/Help';
+import Inbox from '@material-ui/icons/Inbox';
+import Info from '@material-ui/icons/Info';
+import Input from '@material-ui/icons/Input';
+import Menu from '@material-ui/icons/Menu';
+import MoreVert from '@material-ui/icons/MoreVert';
+import Notifications from '@material-ui/icons/Notifications';
+import People from '@material-ui/icons/People';
+import Person from '@material-ui/icons/Person';
+import PersonAdd from '@material-ui/icons/PersonAdd';
+import PlayArrow from '@material-ui/icons/PlayArrow';
+import RateReview from '@material-ui/icons/RateReview';
+import Search from '@material-ui/icons/Search';
+import Star from '@material-ui/icons/Star';
+import StarBorder from '@material-ui/icons/StarBorder';
-export enum IconTypes {
- ANNOUNCEMENT = 'announcement',
- FOLDER = 'folder',
- CLOSE = 'close',
- PROJECT = 'project',
- COLLECTION = 'collection',
- PROCESS = 'process'
-}
+export type IconType = React.SFC<{ className?: string }>;
-interface IconBaseDataProps {
- icon: IconTypes;
- className?: string;
-}
-
-type IconBaseProps = IconBaseDataProps;
-
-interface IconBaseState {
- icon: IconTypes;
-}
-
-const getSpecificIcon = (props: any) => ({
- announcement: <CloseAnnouncement className={props.className} />,
- folder: <FolderIcon className={props.className} />,
- close: <CloseIcon className={props.className} />,
- project: <i className={classnames([props.className, 'fas fa-folder fa-lg'])} />,
- collection: <i className={classnames([props.className, 'fas fa-archive fa-lg'])} />,
- process: <i className={classnames([props.className, 'fas fa-cogs fa-lg'])} />
-});
-
-class IconBase extends React.Component<IconBaseProps, IconBaseState> {
- state = {
- icon: IconTypes.FOLDER,
- };
-
- render() {
- return getSpecificIcon(this.props)[this.props.icon];
- }
-}
-
-export default IconBase;
\ No newline at end of file
+export const AddFavoriteIcon: IconType = (props) => <StarBorder {...props} />;
+export const AdvancedIcon: IconType = (props) => <Folder {...props} />;
+export const CustomizeTableIcon: IconType = (props) => <Menu {...props} />;
+export const CopyIcon: IconType = (props) => <ContentCopy {...props} />;
+export const CollectionIcon: IconType = (props) => <Folder {...props} />;
+export const CloseIcon: IconType = (props) => <Close {...props} />;
+export const DefaultIcon: IconType = (props) => <RateReview {...props} />;
+export const DetailsIcon: IconType = (props) => <Info {...props} />;
+export const DownloadIcon: IconType = (props) => <GetApp {...props} />;
+export const FavoriteIcon: IconType = (props) => <Star {...props} />;
+export const HelpIcon: IconType = (props) => <Help {...props} />;
+export const LogIcon: IconType = (props) => <Folder {...props} />;
+export const MoreOptionsIcon: IconType = (props) => <MoreVert {...props} />;
+export const MoveToIcon: IconType = (props) => <Input {...props} />;
+export const NewProjectIcon: IconType = (props) => <CreateNewFolder {...props} />;
+export const NotificationIcon: IconType = (props) => <Notifications {...props} />;
+export const PaginationDownIcon: IconType = (props) => <ArrowDropDown {...props} />;
+export const PaginationLeftArrowIcon: IconType = (props) => <ChevronLeft {...props} />;
+export const PaginationRightArrowIcon: IconType = (props) => <ChevronRight {...props} />;
+export const ProcessIcon: IconType = (props) => <BubbleChart {...props} />;
+export const ProjectIcon: IconType = (props) => <Folder {...props} />;
+export const ProjectsIcon: IconType = (props) => <Inbox {...props} />;
+export const ProvenanceGraphIcon: IconType = (props) => <Folder {...props} />;
+export const RecentIcon: IconType = (props) => <AccessTime {...props} />;
+export const RemoveIcon: IconType = (props) => <Delete {...props} />;
+export const RemoveFavoriteIcon: IconType = (props) => <Star {...props} />;
+export const RenameIcon: IconType = (props) => <Edit {...props} />;
+export const ReRunProcessIcon: IconType = (props) => <Cached {...props} />;
+export const SearchIcon: IconType = (props) => <Search {...props} />;
+export const ShareIcon: IconType = (props) => <PersonAdd {...props} />;
+export const ShareMeIcon: IconType = (props) => <People {...props} />;
+export const SidePanelRightArrowIcon: IconType = (props) => <PlayArrow {...props} />;
+export const TrashIcon: IconType = (props) => <Delete {...props} />;
+export const UserPanelIcon: IconType = (props) => <Person {...props} />;
+export const UsedByIcon: IconType = (props) => <Folder {...props} />;
+export const WorkflowIcon: IconType = (props) => <Code {...props} />;
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import { ArvadosTheme } from '../../common/custom-theme';
+import { ListItemIcon, ListItemText, Typography } from '@material-ui/core';
+import { IconType } from '../icon/icon';
+import * as classnames from "classnames";
+
+type CssRules = 'root' | 'listItemText' | 'hasMargin' | 'active';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ display: 'flex',
+ alignItems: 'center'
+ },
+ listItemText: {
+ fontWeight: 700
+ },
+ active: {
+ color: theme.palette.primary.main,
+ },
+ hasMargin: {
+ marginLeft: '18px',
+ },
+});
+
+export interface ListItemTextIconDataProps {
+ icon: IconType;
+ name: string;
+ isActive?: boolean;
+ hasMargin?: boolean;
+}
+
+type ListItemTextIconProps = ListItemTextIconDataProps & WithStyles<CssRules>;
+
+export const ListItemTextIcon = withStyles(styles)(
+ class extends React.Component<ListItemTextIconProps, {}> {
+ render() {
+ const { classes, isActive, hasMargin, name, icon: Icon } = this.props;
+ return (
+ <Typography component='span' className={classes.root}>
+ <ListItemIcon className={classnames({
+ [classes.hasMargin]: hasMargin,
+ [classes.active]: isActive
+ })}>
+ <Icon />
+ </ListItemIcon>
+ <ListItemText primary={
+ <Typography variant='body1' className={classnames(classes.listItemText, {
+ [classes.active]: isActive
+ })}>
+ {name}
+ </Typography>
+ } />
+ </Typography>
+ );
+ }
+ }
+);
import { PopoverOrigin } from "@material-ui/core/Popover";
-export const mockAnchorFromMouseEvent = (event: React.MouseEvent<HTMLElement>) => {
+export const createAnchorAt = (position: {x: number, y: number}) => {
const el = document.createElement('div');
const clientRect = {
- left: event.clientX,
- right: event.clientX,
- top: event.clientY,
- bottom: event.clientY,
+ left: position.x,
+ right: position.x,
+ top: position.y,
+ bottom: position.y,
width: 0,
height: 0
};
import { mount, configure } from "enzyme";
import * as Adapter from "enzyme-adapter-react-16";
-import Popover, { DefaultTrigger } from "./popover";
+import { Popover, DefaultTrigger } from "./popover";
import Button, { ButtonProps } from "@material-ui/core/Button";
configure({ adapter: new Adapter() });
popover.find(DefaultTrigger).simulate("click");
expect(popover.find(CustomTrigger)).toHaveLength(1);
});
-
+
it("does not close if closeOnContentClick is not set", () => {
const popover = mount(
<Popover>
<Button {...props}>
Open popover
</Button>
-);
\ No newline at end of file
+);
closeOnContentClick?: boolean;
}
-
-class Popover extends React.Component<PopoverProps> {
-
+export class Popover extends React.Component<PopoverProps> {
state = {
anchorEl: undefined
};
this.handleClose();
}
}
-
}
export const DefaultTrigger: React.SFC<IconButtonProps> = (props) => (
<i className="fas" />
</IconButton>
);
-
-export default Popover;
import * as React from "react";
import { mount, configure } from "enzyme";
-import SearchBar, { DEFAULT_SEARCH_DEBOUNCE } from "./search-bar";
+import { SearchBar, DEFAULT_SEARCH_DEBOUNCE } from "./search-bar";
import * as Adapter from 'enzyme-adapter-react-16';
jest.advanceTimersByTime(DEFAULT_SEARCH_DEBOUNCE);
expect(onSearch).toBeCalledWith("current value");
});
-
+
it("calls onSearch after the time specified in props has passed", () => {
const searchBar = mount(<SearchBar value="" onSearch={onSearch} debounce={2000}/>);
searchBar.find("input").simulate("change", { target: { value: "current value" } });
jest.advanceTimersByTime(1000);
expect(onSearch).toBeCalledWith("current value");
});
-
+
it("calls onSearch only once after no change happened during the specified time", () => {
const searchBar = mount(<SearchBar value="" onSearch={onSearch} debounce={1000}/>);
searchBar.find("input").simulate("change", { target: { value: "current value" } });
jest.advanceTimersByTime(1000);
expect(onSearch).toHaveBeenCalledTimes(1);
});
-
+
it("calls onSearch again after the specified time has passed since previous call", () => {
const searchBar = mount(<SearchBar value="" onSearch={onSearch} debounce={1000}/>);
searchBar.find("input").simulate("change", { target: { value: "current value" } });
jest.advanceTimersByTime(1000);
expect(onSearch).toBeCalledWith("latest value");
expect(onSearch).toHaveBeenCalledTimes(2);
-
- });
+ });
});
-
-});
\ No newline at end of file
+});
import { IconButton, Paper, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core';
import SearchIcon from '@material-ui/icons/Search';
-interface SearchBarDataProps {
- value: string;
-}
-
-interface SearchBarActionProps {
- onSearch: (value: string) => any;
- debounce?: number;
-}
-
-type SearchBarProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
-
-interface SearchBarState {
- value: string;
-}
-
-export const DEFAULT_SEARCH_DEBOUNCE = 1000;
-
-class SearchBar extends React.Component<SearchBarProps> {
-
- state: SearchBarState = {
- value: ""
- };
-
- timeout: number;
-
- render() {
- const { classes } = this.props;
- return <Paper className={classes.container}>
- <form onSubmit={this.handleSubmit}>
- <input
- className={classes.input}
- onChange={this.handleChange}
- placeholder="Search"
- value={this.state.value}
- />
- <IconButton className={classes.button}>
- <SearchIcon />
- </IconButton>
- </form>
- </Paper>;
- }
-
- componentDidMount() {
- this.setState({value: this.props.value});
- }
-
- componentWillReceiveProps(nextProps: SearchBarProps) {
- if (nextProps.value !== this.props.value) {
- this.setState({ value: nextProps.value });
- }
- }
-
- componentWillUnmount() {
- clearTimeout(this.timeout);
- }
-
- handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
- event.preventDefault();
- clearTimeout(this.timeout);
- this.props.onSearch(this.state.value);
- }
-
- handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
- clearTimeout(this.timeout);
- this.setState({ value: event.target.value });
- this.timeout = window.setTimeout(
- () => this.props.onSearch(this.state.value),
- this.props.debounce || DEFAULT_SEARCH_DEBOUNCE
- );
-
- }
-
-}
-
type CssRules = 'container' | 'input' | 'button';
const styles: StyleRulesCallback<CssRules> = theme => {
};
};
-export default withStyles(styles)(SearchBar);
\ No newline at end of file
+interface SearchBarDataProps {
+ value: string;
+}
+
+interface SearchBarActionProps {
+ onSearch: (value: string) => any;
+ debounce?: number;
+}
+
+type SearchBarProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
+
+interface SearchBarState {
+ value: string;
+}
+
+export const DEFAULT_SEARCH_DEBOUNCE = 1000;
+
+export const SearchBar = withStyles(styles)(
+ class extends React.Component<SearchBarProps> {
+ state: SearchBarState = {
+ value: ""
+ };
+
+ timeout: number;
+
+ render() {
+ const {classes} = this.props;
+ return <Paper className={classes.container}>
+ <form onSubmit={this.handleSubmit}>
+ <input
+ className={classes.input}
+ onChange={this.handleChange}
+ placeholder="Search"
+ value={this.state.value}
+ />
+ <IconButton className={classes.button}>
+ <SearchIcon/>
+ </IconButton>
+ </form>
+ </Paper>;
+ }
+
+ componentDidMount() {
+ this.setState({value: this.props.value});
+ }
+
+ componentWillReceiveProps(nextProps: SearchBarProps) {
+ if (nextProps.value !== this.props.value) {
+ this.setState({value: nextProps.value});
+ }
+ }
+
+ componentWillUnmount() {
+ clearTimeout(this.timeout);
+ }
+
+ handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+ event.preventDefault();
+ clearTimeout(this.timeout);
+ this.props.onSearch(this.state.value);
+ }
+
+ handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ clearTimeout(this.timeout);
+ this.setState({value: event.target.value});
+ this.timeout = window.setTimeout(
+ () => this.props.onSearch(this.state.value),
+ this.props.debounce || DEFAULT_SEARCH_DEBOUNCE
+ );
+
+ }
+ }
+);
import * as React from "react";
import { mount, configure } from "enzyme";
-import SearchInput, { DEFAULT_SEARCH_DEBOUNCE } from "./search-input";
+import { SearchInput, DEFAULT_SEARCH_DEBOUNCE } from "./search-input";
import * as Adapter from 'enzyme-adapter-react-16';
jest.advanceTimersByTime(DEFAULT_SEARCH_DEBOUNCE);
expect(onSearch).toBeCalledWith("current value");
});
-
+
it("calls onSearch after the time specified in props has passed", () => {
const searchInput = mount(<SearchInput value="" onSearch={onSearch} debounce={2000}/>);
searchInput.find("input").simulate("change", { target: { value: "current value" } });
jest.advanceTimersByTime(1000);
expect(onSearch).toBeCalledWith("current value");
});
-
+
it("calls onSearch only once after no change happened during the specified time", () => {
const searchInput = mount(<SearchInput value="" onSearch={onSearch} debounce={1000}/>);
searchInput.find("input").simulate("change", { target: { value: "current value" } });
jest.advanceTimersByTime(1000);
expect(onSearch).toHaveBeenCalledTimes(1);
});
-
+
it("calls onSearch again after the specified time has passed since previous call", () => {
const searchInput = mount(<SearchInput value="" onSearch={onSearch} debounce={1000}/>);
searchInput.find("input").simulate("change", { target: { value: "current value" } });
jest.advanceTimersByTime(1000);
expect(onSearch).toBeCalledWith("latest value");
expect(onSearch).toHaveBeenCalledTimes(2);
-
+
});
});
-});
\ No newline at end of file
+});
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { IconButton, Paper, StyleRulesCallback, withStyles, WithStyles, FormControl, InputLabel, Input, InputAdornment, FormHelperText } from '@material-ui/core';
+import { IconButton, StyleRulesCallback, withStyles, WithStyles, FormControl, InputLabel, Input, InputAdornment } from '@material-ui/core';
import SearchIcon from '@material-ui/icons/Search';
-interface SearchInputDataProps {
- value: string;
-}
-
-interface SearchInputActionProps {
- onSearch: (value: string) => any;
- debounce?: number;
-}
-
-type SearchInputProps = SearchInputDataProps & SearchInputActionProps & WithStyles<CssRules>;
-
-interface SearchInputState {
- value: string;
-}
-
-export const DEFAULT_SEARCH_DEBOUNCE = 1000;
-
-class SearchInput extends React.Component<SearchInputProps> {
-
- state: SearchInputState = {
- value: ""
- };
-
- timeout: number;
-
- render() {
- const { classes } = this.props;
- return <form onSubmit={this.handleSubmit}>
- <FormControl>
- <InputLabel>Search</InputLabel>
- <Input
- type="text"
- value={this.state.value}
- onChange={this.handleChange}
- endAdornment={
- <InputAdornment position="end">
- <IconButton
- onClick={this.handleSubmit}>
- <SearchIcon />
- </IconButton>
- </InputAdornment>
- } />
- </FormControl>
- </form>;
- }
-
- componentDidMount() {
- this.setState({ value: this.props.value });
- }
-
- componentWillReceiveProps(nextProps: SearchInputProps) {
- if (nextProps.value !== this.props.value) {
- this.setState({ value: nextProps.value });
- }
- }
-
- componentWillUnmount() {
- clearTimeout(this.timeout);
- }
-
- handleSubmit = (event: React.FormEvent<HTMLElement>) => {
- event.preventDefault();
- clearTimeout(this.timeout);
- this.props.onSearch(this.state.value);
- }
-
- handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
- clearTimeout(this.timeout);
- this.setState({ value: event.target.value });
- this.timeout = window.setTimeout(
- () => this.props.onSearch(this.state.value),
- this.props.debounce || DEFAULT_SEARCH_DEBOUNCE
- );
-
- }
-
-}
-
type CssRules = 'container' | 'input' | 'button';
const styles: StyleRulesCallback<CssRules> = theme => {
};
};
-export default withStyles(styles)(SearchInput);
\ No newline at end of file
+interface SearchInputDataProps {
+ value: string;
+}
+
+interface SearchInputActionProps {
+ onSearch: (value: string) => any;
+ debounce?: number;
+}
+
+type SearchInputProps = SearchInputDataProps & SearchInputActionProps & WithStyles<CssRules>;
+
+interface SearchInputState {
+ value: string;
+}
+
+export const DEFAULT_SEARCH_DEBOUNCE = 1000;
+
+export const SearchInput = withStyles(styles)(
+ class extends React.Component<SearchInputProps> {
+ state: SearchInputState = {
+ value: ""
+ };
+
+ timeout: number;
+
+ render() {
+ const { classes } = this.props;
+ return <form onSubmit={this.handleSubmit}>
+ <FormControl>
+ <InputLabel>Search</InputLabel>
+ <Input
+ type="text"
+ value={this.state.value}
+ onChange={this.handleChange}
+ endAdornment={
+ <InputAdornment position="end">
+ <IconButton
+ onClick={this.handleSubmit}>
+ <SearchIcon/>
+ </IconButton>
+ </InputAdornment>
+ }/>
+ </FormControl>
+ </form>;
+ }
+
+ componentDidMount() {
+ this.setState({ value: this.props.value });
+ }
+
+ componentWillReceiveProps(nextProps: SearchInputProps) {
+ if (nextProps.value !== this.props.value) {
+ this.setState({ value: nextProps.value });
+ }
+ }
+
+ componentWillUnmount() {
+ clearTimeout(this.timeout);
+ }
+
+ handleSubmit = (event: React.FormEvent<HTMLElement>) => {
+ event.preventDefault();
+ clearTimeout(this.timeout);
+ this.props.onSearch(this.state.value);
+ }
+
+ handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ clearTimeout(this.timeout);
+ this.setState({ value: event.target.value });
+ this.timeout = window.setTimeout(
+ () => this.props.onSearch(this.state.value),
+ this.props.debounce || DEFAULT_SEARCH_DEBOUNCE
+ );
+
+ }
+ }
+);
import * as React from 'react';
import { ReactElement } from 'react';
-import { StyleRulesCallback, Theme, WithStyles, withStyles } from '@material-ui/core/styles';
-import List from "@material-ui/core/List/List";
-import ListItem from "@material-ui/core/ListItem/ListItem";
-import ListItemText from "@material-ui/core/ListItemText/ListItemText";
-import ListItemIcon from '@material-ui/core/ListItemIcon';
-import Collapse from "@material-ui/core/Collapse/Collapse";
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import { ArvadosTheme } from '../../common/custom-theme';
+import { List, ListItem, ListItemIcon, Collapse } from "@material-ui/core";
+import { SidePanelRightArrowIcon, IconType } from '../icon/icon';
+import * as classnames from "classnames";
+import { ListItemTextIcon } from '../list-item-text-icon/list-item-text-icon';
-import { Typography } from '@material-ui/core';
+type CssRules = 'active' | 'row' | 'root' | 'list' | 'iconClose' | 'iconOpen' | 'toggableIconContainer' | 'toggableIcon';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ overflowY: 'auto',
+ minWidth: '240px',
+ whiteSpace: 'nowrap',
+ marginTop: '52px',
+ display: 'flex',
+ flexGrow: 1,
+ },
+ list: {
+ padding: '5px 0px 5px 14px',
+ minWidth: '240px',
+ },
+ row: {
+ display: 'flex',
+ alignItems: 'center',
+ },
+ toggableIconContainer: {
+ color: theme.palette.grey["700"],
+ height: '14px',
+ position: 'absolute'
+ },
+ toggableIcon: {
+ fontSize: '14px'
+ },
+ active: {
+ color: theme.palette.primary.main,
+ },
+ iconClose: {
+ transition: 'all 0.1s ease',
+ },
+ iconOpen: {
+ transition: 'all 0.1s ease',
+ transform: 'rotate(90deg)',
+ }
+});
export interface SidePanelItem {
id: string;
name: string;
- icon: string;
+ icon: IconType;
active?: boolean;
open?: boolean;
margin?: boolean;
openAble?: boolean;
}
-interface SidePanelProps {
+interface SidePanelDataProps {
toggleOpen: (id: string) => void;
toggleActive: (id: string) => void;
sidePanelItems: SidePanelItem[];
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: SidePanelItem) => void;
}
-class SidePanel extends React.Component<SidePanelProps & WithStyles<CssRules>> {
- render(): ReactElement<any> {
- const { classes, toggleOpen, toggleActive, sidePanelItems, children } = this.props;
- const { listItemText, leftSidePanelContainer, row, list, icon, projectIconMargin, active, activeArrow, inactiveArrow, arrowTransition, arrowRotate } = classes;
- return (
- <div className={leftSidePanelContainer}>
- <List>
- {sidePanelItems.map(it => (
- <span key={it.name}>
- <ListItem button className={list} onClick={() => toggleActive(it.id)} onContextMenu={this.handleRowContextMenu(it)}>
- <span className={row}>
- {it.openAble ? <i onClick={() => toggleOpen(it.id)} className={`${it.active ? activeArrow : inactiveArrow}
- ${it.open ? `fas fa-caret-down ${arrowTransition}` : `fas fa-caret-down ${arrowRotate}`}`} /> : null}
- <ListItemIcon className={it.active ? active : ''}>
- <i className={`${it.icon} ${icon} ${it.margin ? projectIconMargin : ''}`} />
- </ListItemIcon>
- <ListItemText className={listItemText} primary={<Typography className={it.active ? active : ''}>{it.name}</Typography>} />
- </span>
- </ListItem>
- {it.openAble ? (
- <Collapse in={it.open} timeout="auto" unmountOnExit>
- {children}
- </Collapse>) : null}
- </span>
- ))}
- </List>
- </div>
- );
- }
-
- handleRowContextMenu = (item: SidePanelItem) =>
- (event: React.MouseEvent<HTMLElement>) =>
- item.openAble ? this.props.onContextMenu(event, item) : null
+type SidePanelProps = SidePanelDataProps & WithStyles<CssRules>;
-}
+export const SidePanel = withStyles(styles)(
+ class extends React.Component<SidePanelProps> {
+ render(): ReactElement<any> {
+ const { classes, toggleOpen, toggleActive, sidePanelItems, children } = this.props;
+ const { root, row, list, toggableIconContainer } = classes;
+ return (
+ <div className={root}>
+ <List>
+ {sidePanelItems.map(it => (
+ <span key={it.name}>
+ <ListItem button className={list} onClick={() => toggleActive(it.id)}
+ onContextMenu={this.handleRowContextMenu(it)}>
+ <span className={row}>
+ {it.openAble ? (
+ <i onClick={() => toggleOpen(it.id)} className={toggableIconContainer}>
+ <ListItemIcon
+ className={this.getToggableIconClassNames(it.open, it.active)}>
+ < SidePanelRightArrowIcon/>
+ </ListItemIcon>
+ </i>
+ ) : null}
+ <ListItemTextIcon icon={it.icon} name={it.name} isActive={it.active}
+ hasMargin={it.margin}/>
+ </span>
+ </ListItem>
+ {it.openAble ? (
+ <Collapse in={it.open} timeout="auto" unmountOnExit>
+ {children}
+ </Collapse>
+ ) : null}
+ </span>
+ ))}
+ </List>
+ </div>
+ );
+ }
-type CssRules = 'active' | 'listItemText' | 'row' | 'leftSidePanelContainer' | 'list' | 'icon' | 'projectIconMargin' |
- 'activeArrow' | 'inactiveArrow' | 'arrowRotate' | 'arrowTransition';
+ getToggableIconClassNames = (isOpen?: boolean, isActive ?: boolean) => {
+ const { classes } = this.props;
+ return classnames(classes.toggableIcon, {
+ [classes.iconOpen]: isOpen,
+ [classes.iconClose]: !isOpen,
+ [classes.active]: isActive
+ });
+ }
-const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
- active: {
- color: '#4285F6',
- },
- listItemText: {
- padding: '0px',
- },
- row: {
- display: 'flex',
- alignItems: 'center',
- },
- activeArrow: {
- color: '#4285F6',
- position: 'absolute',
- },
- inactiveArrow: {
- position: 'absolute',
- },
- arrowTransition: {
- transition: 'all 0.1s ease',
- },
- arrowRotate: {
- transition: 'all 0.1s ease',
- transform: 'rotate(-90deg)',
- },
- leftSidePanelContainer: {
- overflowY: 'auto',
- minWidth: '240px',
- whiteSpace: 'nowrap',
- marginTop: '52px',
- display: 'flex',
- flexGrow: 1,
- },
- list: {
- paddingBottom: '5px',
- paddingTop: '5px',
- paddingLeft: '14px',
- minWidth: '240px',
- },
- icon: {
- minWidth: '20px',
- },
- projectIconMargin: {
- marginLeft: '17px',
+ handleRowContextMenu = (item: SidePanelItem) =>
+ (event: React.MouseEvent<HTMLElement>) =>
+ item.openAble ? this.props.onContextMenu(event, item) : null
}
-});
-
-export default withStyles(styles)(SidePanel);
\ No newline at end of file
+);
import * as Adapter from 'enzyme-adapter-react-16';
import ListItem from "@material-ui/core/ListItem/ListItem";
-import Tree, { TreeItem } from './tree';
+import { Tree, TreeItem } from './tree';
import { ProjectResource } from '../../models/project';
import { mockProjectResource } from '../../models/test-utils';
import * as React from 'react';
import List from "@material-ui/core/List/List";
import ListItem from "@material-ui/core/ListItem/ListItem";
-import { StyleRulesCallback, Theme, withStyles, WithStyles } from '@material-ui/core/styles';
+import { StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
import { ReactElement } from "react";
import Collapse from "@material-ui/core/Collapse/Collapse";
import CircularProgress from '@material-ui/core/CircularProgress';
+import { ArvadosTheme } from '../../common/custom-theme';
+
+type CssRules = 'list' | 'activeArrow' | 'inactiveArrow' | 'arrowRotate' | 'arrowTransition' | 'loader' | 'arrowVisibility';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ list: {
+ paddingBottom: '3px',
+ paddingTop: '3px',
+ },
+ activeArrow: {
+ color: theme.palette.primary.main,
+ position: 'absolute',
+ },
+ inactiveArrow: {
+ color: theme.palette.grey["700"],
+ position: 'absolute',
+ },
+ arrowTransition: {
+ transition: 'all 0.1s ease',
+ },
+ arrowRotate: {
+ transition: 'all 0.1s ease',
+ transform: 'rotate(-90deg)',
+ },
+ arrowVisibility: {
+ opacity: 0,
+ },
+ loader: {
+ position: 'absolute',
+ transform: 'translate(0px)',
+ top: '3px'
+ }
+});
export enum TreeItemStatus {
Initial,
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
}
-class Tree<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
- render(): ReactElement<any> {
- const level = this.props.level ? this.props.level : 0;
- const { classes, render, toggleItemOpen, items, toggleItemActive, onContextMenu } = this.props;
- const { list, inactiveArrow, activeArrow, loader } = classes;
- return <List component="div" className={list}>
- {items && items.map((it: TreeItem<T>, idx: number) =>
- <div key={`item/${level}/${idx}`}>
- <ListItem button className={list} style={{ paddingLeft: (level + 1) * 20 }} onClick={() => toggleItemActive(it.id, it.status)} onContextMenu={this.handleRowContextMenu(it)}>
- {it.status === TreeItemStatus.Pending ? <CircularProgress size={10} className={loader} /> : null}
- {it.toggled && it.items && it.items.length === 0 ? null : this.renderArrow(it.status, it.active ? activeArrow : inactiveArrow, it.open, it.id)}
- {render(it, level)}
- </ListItem>
- {it.items && it.items.length > 0 &&
+export const Tree = withStyles(styles)(
+ class Component<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
+ render(): ReactElement<any> {
+ const level = this.props.level ? this.props.level : 0;
+ const { classes, render, toggleItemOpen, items, toggleItemActive, onContextMenu } = this.props;
+ const { list, inactiveArrow, activeArrow, loader } = classes;
+ return <List component="div" className={list}>
+ {items && items.map((it: TreeItem<T>, idx: number) =>
+ <div key={`item/${level}/${idx}`}>
+ <ListItem button className={list} style={{ paddingLeft: (level + 1) * 20 }}
+ onClick={() => toggleItemActive(it.id, it.status)}
+ onContextMenu={this.handleRowContextMenu(it)}>
+ {it.status === TreeItemStatus.Pending ?
+ <CircularProgress size={10} className={loader}/> : null}
+ {it.toggled && it.items && it.items.length === 0 ? null : this.renderArrow(it.status, it.active ? activeArrow : inactiveArrow, it.open, it.id)}
+ {render(it, level)}
+ </ListItem>
+ {it.items && it.items.length > 0 &&
<Collapse in={it.open} timeout="auto" unmountOnExit>
- <StyledTree
+ <Tree
items={it.items}
render={render}
toggleItemOpen={toggleItemOpen}
toggleItemActive={toggleItemActive}
level={level + 1}
- onContextMenu={onContextMenu} />
+ onContextMenu={onContextMenu}/>
</Collapse>}
- </div>)}
- </List>;
- }
- renderArrow(status: TreeItemStatus, arrowClass: string, open: boolean, id: string) {
- const { arrowTransition, arrowVisibility, arrowRotate } = this.props.classes;
- return <i onClick={() => this.props.toggleItemOpen(id, status)}
- className={`
- ${arrowClass}
- ${status === TreeItemStatus.Pending ? arrowVisibility : ''}
- ${open ? `fas fa-caret-down ${arrowTransition}` : `fas fa-caret-down ${arrowRotate}`}`} />;
- }
-
- handleRowContextMenu = (item: TreeItem<T>) =>
- (event: React.MouseEvent<HTMLElement>) =>
- this.props.onContextMenu(event, item)
-}
+ </div>)}
+ </List>;
+ }
-type CssRules = 'list' | 'activeArrow' | 'inactiveArrow' | 'arrowRotate' | 'arrowTransition' | 'loader' | 'arrowVisibility';
+ renderArrow(status: TreeItemStatus, arrowClass: string, open: boolean, id: string) {
+ const { arrowTransition, arrowVisibility, arrowRotate } = this.props.classes;
+ return <i onClick={() => this.props.toggleItemOpen(id, status)}
+ className={`
+ ${arrowClass}
+ ${status === TreeItemStatus.Pending ? arrowVisibility : ''}
+ ${open ? `fas fa-caret-down ${arrowTransition}` : `fas fa-caret-down ${arrowRotate}`}`}/>;
+ }
-const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
- list: {
- paddingBottom: '3px',
- paddingTop: '3px',
- },
- activeArrow: {
- color: '#4285F6',
- position: 'absolute',
- },
- inactiveArrow: {
- position: 'absolute',
- },
- arrowTransition: {
- transition: 'all 0.1s ease',
- },
- arrowRotate: {
- transition: 'all 0.1s ease',
- transform: 'rotate(-90deg)',
- },
- arrowVisibility: {
- opacity: 0,
- },
- loader: {
- position: 'absolute',
- transform: 'translate(0px)',
- top: '3px'
+ handleRowContextMenu = (item: TreeItem<T>) =>
+ (event: React.MouseEvent<HTMLElement>) =>
+ this.props.onContextMenu(event, item)
}
-});
-
-const StyledTree = withStyles(styles)(Tree);
-export default StyledTree;
+);
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from "react-redux";
-import Workbench from './views/workbench/workbench';
+import { Workbench } from './views/workbench/workbench';
import './index.css';
import { Route } from "react-router";
import createBrowserHistory from "history/createBrowserHistory";
-import configureStore from "./store/store";
+import { configureStore } from "./store/store";
import { ConnectedRouter } from "react-router-redux";
-import ApiToken from "./views-components/api-token/api-token";
-import authActions from "./store/auth/auth-action";
+import { ApiToken } from "./views-components/api-token/api-token";
+import { authActions } from "./store/auth/auth-action";
import { authService } from "./services/services";
import { getProjectList } from "./store/project/project-action";
import { MuiThemeProvider } from '@material-ui/core/styles';
import { CustomTheme } from './common/custom-theme';
-import CommonResourceService from './common/api/common-resource-service';
-import { CollectionResource } from './models/collection';
-import { serverApi } from './common/api/server-api';
-import { ProcessResource } from './models/process';
-
-const history = createBrowserHistory();
-
-const store = configureStore(history);
-
-store.dispatch(authActions.INIT());
-store.dispatch<any>(getProjectList(authService.getUuid()));
-
-// const service = new CommonResourceService<CollectionResource>(serverApi, "collections");
-// service.create({ ownerUuid: "qr1hi-j7d0g-u55bcc7fa5w7v4p", name: "Collection 1 short title"});
-// service.create({ ownerUuid: "qr1hi-j7d0g-u55bcc7fa5w7v4p", name: "Collection 2 long long long title"});
-
-// const processService = new CommonResourceService<ProcessResource>(serverApi, "container_requests");
-// processService.create({ ownerUuid: "qr1hi-j7d0g-u55bcc7fa5w7v4p", name: "Process 1 short title"});
-// processService.create({ ownerUuid: "qr1hi-j7d0g-u55bcc7fa5w7v4p", name: "Process 2 long long long title" });
-
-const App = () =>
- <MuiThemeProvider theme={CustomTheme}>
- <Provider store={store}>
- <ConnectedRouter history={history}>
- <div>
- <Route path="/" component={Workbench} />
- <Route path="/token" component={ApiToken} />
- </div>
- </ConnectedRouter>
- </Provider>
- </MuiThemeProvider>;
-
-ReactDOM.render(
- <App />,
- document.getElementById('root') as HTMLElement
-);
+import { fetchConfig } from './common/config';
+import { setBaseUrl } from './common/api/server-api';
+import { addMenuActionSet, ContextMenuKind } from "./views-components/context-menu/context-menu";
+import { rootProjectActionSet } from "./views-components/context-menu/action-sets/root-project-action-set";
+import { projectActionSet } from "./views-components/context-menu/action-sets/project-action-set";
+
+addMenuActionSet(ContextMenuKind.RootProject, rootProjectActionSet);
+addMenuActionSet(ContextMenuKind.Project, projectActionSet);
+
+fetchConfig()
+ .then(config => {
+
+ setBaseUrl(config.API_HOST);
+
+ const history = createBrowserHistory();
+ const store = configureStore(history);
+
+ store.dispatch(authActions.INIT());
+ store.dispatch<any>(getProjectList(authService.getUuid()));
+
+ const App = () =>
+ <MuiThemeProvider theme={CustomTheme}>
+ <Provider store={store}>
+ <ConnectedRouter history={history}>
+ <div>
+ <Route path="/" component={Workbench} />
+ <Route path="/token" component={ApiToken} />
+ </div>
+ </ConnectedRouter>
+ </Provider>
+ </MuiThemeProvider>;
+
+ ReactDOM.render(
+ <App />,
+ document.getElementById('root') as HTMLElement
+ );
+ });
+
+
logUuid: string;
outputUuid: string;
filters: string;
-
-}
\ No newline at end of file
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ProjectResource } from "./project";
+import { CollectionResource } from "./collection";
+import { ProcessResource } from "./process";
+import { EmptyResource } from "./empty";
+
+export type DetailsResource = ProjectResource | CollectionResource | ProcessResource | EmptyResource;
//
// SPDX-License-Identifier: AGPL-3.0
-import { ResourceKind } from "./resource";
-
export interface EmptyResource {
name: string;
kind: undefined;
-}
\ No newline at end of file
+}
//
// SPDX-License-Identifier: AGPL-3.0
-import { API_HOST, serverApi } from "../../common/api/server-api";
+import { API_HOST } from "../../common/api/server-api";
import { User } from "../../models/user";
+import { AxiosInstance } from "../../../node_modules/axios";
export const API_TOKEN_KEY = 'apiToken';
export const USER_EMAIL_KEY = 'userEmail';
is_admin: boolean;
}
-export default class AuthService {
+export class AuthService {
+
+ constructor(protected serverApi: AxiosInstance) { }
public saveApiToken(token: string) {
localStorage.setItem(API_TOKEN_KEY, token);
}
public getUserDetails = (): Promise<User> => {
- return serverApi
+ return this.serverApi
.get<UserDetailsResponse>('/users/current')
.then(resp => ({
email: resp.data.email,
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
-import GroupsService from "./groups-service";
+import { GroupsService } from "./groups-service";
describe("GroupsService", () => {
// 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 { FilterBuilder } from "../../common/api/filter-builder";
+import { OrderBuilder } from "../../common/api/order-builder";
import { AxiosInstance } from "axios";
import { GroupResource } from "../../models/group";
import { CollectionResource } from "../../models/collection";
ProjectResource |
ProcessResource;
-export default class GroupsService<T extends GroupResource = GroupResource> extends CommonResourceService<T> {
+export class GroupsService<T extends GroupResource = GroupResource> extends CommonResourceService<T> {
constructor(serverApi: AxiosInstance) {
super(serverApi, "groups");
Collection = "collections",
Project = "groups",
Process = "container_requests"
-}
\ No newline at end of file
+}
// SPDX-License-Identifier: AGPL-3.0
import axios from "axios";
-import MockAdapter from "axios-mock-adapter";
-import ProjectService from "./project-service";
-import FilterBuilder from "../../common/api/filter-builder";
+import { ProjectService } from "./project-service";
+import { FilterBuilder } from "../../common/api/filter-builder";
import { ProjectResource } from "../../models/project";
describe("CommonResourceService", () => {
}
});
});
-
+
});
//
// SPDX-License-Identifier: AGPL-3.0
-import GroupsService, { ContentsArguments } from "../groups-service/groups-service";
+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 { FilterBuilder } from "../../common/api/filter-builder";
-export default class ProjectService extends GroupsService<ProjectResource> {
+export class ProjectService extends GroupsService<ProjectResource> {
create(data: Partial<ProjectResource>) {
const projectData = { ...data, groupClass: GroupClass.Project };
.addEqual("groupClass", GroupClass.Project));
}
-}
\ No newline at end of file
+}
//
// SPDX-License-Identifier: AGPL-3.0
-import AuthService from "./auth-service/auth-service";
-import GroupsService from "./groups-service/groups-service";
+import { AuthService } from "./auth-service/auth-service";
+import { GroupsService } from "./groups-service/groups-service";
import { serverApi } from "../common/api/server-api";
-import ProjectService from "./project-service/project-service";
+import { ProjectService } from "./project-service/project-service";
-export const authService = new AuthService();
+export const authService = new AuthService(serverApi);
export const groupsService = new GroupsService(serverApi);
export const projectService = new ProjectService(serverApi);
import { authService } from "../../services/services";
import { User } from "../../models/user";
-const actions = unionize({
+export const authActions = unionize({
SAVE_API_TOKEN: ofType<string>(),
LOGIN: {},
LOGOUT: {},
});
export const getUserDetails = () => (dispatch: Dispatch): Promise<User> => {
- dispatch(actions.USER_DETAILS_REQUEST());
+ dispatch(authActions.USER_DETAILS_REQUEST());
return authService.getUserDetails().then(details => {
- dispatch(actions.USER_DETAILS_SUCCESS(details));
+ dispatch(authActions.USER_DETAILS_SUCCESS(details));
return details;
});
};
-
-export type AuthAction = UnionOf<typeof actions>;
-export default actions;
+export type AuthAction = UnionOf<typeof authActions>;
//
// SPDX-License-Identifier: AGPL-3.0
-import authReducer from "./auth-reducer";
-import actions from "./auth-action";
+import { authReducer } from "./auth-reducer";
+import { authActions } from "./auth-action";
import {
API_TOKEN_KEY,
USER_EMAIL_KEY,
it('should return default state on initialisation', () => {
const initialState = undefined;
- const state = authReducer(initialState, actions.INIT());
+ const state = authReducer(initialState, authActions.INIT());
expect(state).toEqual({
apiToken: undefined,
user: undefined
localStorage.setItem(USER_UUID_KEY, "uuid");
localStorage.setItem(USER_OWNER_UUID_KEY, "ownerUuid");
- const state = authReducer(initialState, actions.INIT());
+ const state = authReducer(initialState, authActions.INIT());
expect(state).toEqual({
apiToken: "token",
user: {
it('should store token in local storage', () => {
const initialState = undefined;
- const state = authReducer(initialState, actions.SAVE_API_TOKEN("token"));
+ const state = authReducer(initialState, authActions.SAVE_API_TOKEN("token"));
expect(state).toEqual({
apiToken: "token",
user: undefined
ownerUuid: "ownerUuid"
};
- const state = authReducer(initialState, actions.USER_DETAILS_SUCCESS(user));
+ const state = authReducer(initialState, authActions.USER_DETAILS_SUCCESS(user));
expect(state).toEqual({
apiToken: undefined,
user: {
it('should fire external url to login', () => {
const initialState = undefined;
window.location.assign = jest.fn();
- authReducer(initialState, actions.LOGIN());
+ authReducer(initialState, authActions.LOGIN());
expect(window.location.assign).toBeCalledWith(
`${API_HOST}/login?return_to=${window.location.protocol}//${window.location.host}/token`
);
it('should fire external url to logout', () => {
const initialState = undefined;
window.location.assign = jest.fn();
- authReducer(initialState, actions.LOGOUT());
+ authReducer(initialState, authActions.LOGOUT());
expect(window.location.assign).toBeCalledWith(
`${API_HOST}/logout?return_to=${location.protocol}//${location.host}`
);
//
// SPDX-License-Identifier: AGPL-3.0
-import actions, { AuthAction } from "./auth-action";
+import { authActions, AuthAction } from "./auth-action";
import { User } from "../../models/user";
import { authService } from "../../services/services";
import { removeServerApiAuthorizationHeader, setServerApiAuthorizationHeader } from "../../common/api/server-api";
apiToken?: string;
}
-const authReducer = (state: AuthState = {}, action: AuthAction) => {
- return actions.match(action, {
+export const authReducer = (state: AuthState = {}, action: AuthAction) => {
+ return authActions.match(action, {
SAVE_API_TOKEN: (token: string) => {
authService.saveApiToken(token);
setServerApiAuthorizationHeader(token);
default: () => state
});
};
-
-export default authReducer;
//
// SPDX-License-Identifier: AGPL-3.0
-// import { default as unionize, ofType, UnionOf } from "unionize";
+import { default as unionize, ofType, UnionOf } from "unionize";
+import { ContextMenuPosition, ContextMenuResource } from "./context-menu-reducer";
-// const actions = unionize({
-// OPEN_CONTEXT_MENU: ofType<{position: {x: number, y: number}}>()
-// }, {
-// tag: 'type',
-// value: 'payload'
-// });
+export const contextMenuActions = unionize({
+ OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
+ CLOSE_CONTEXT_MENU: ofType<{}>()
+}, {
+ tag: 'type',
+ value: 'payload'
+ });
-// export type ContextMenuAction = UnionOf<typeof actions>;
-// export default actions;
\ No newline at end of file
+export type ContextMenuAction = UnionOf<typeof contextMenuActions>;
//
// SPDX-License-Identifier: AGPL-3.0
-// import actions, { DetailsPanelAction } from "./details-panel-action";
-// import { Resource, ResourceKind } from "../../models/resource";
+import { ResourceKind } from "../../models/resource";
+import { contextMenuActions, ContextMenuAction } from "./context-menu-actions";
-// export interface ContextMenuState {
-// position: {
-// x: number;
-// y: number;
-// },
-// resource: {
-// uuid: string;
-// kind: ResourceKind.
-// }
-// }
+export interface ContextMenuState {
+ position: ContextMenuPosition;
+ resource?: ContextMenuResource;
+}
-// const initialState = {
-// item: null,
-// isOpened: false
-// };
+export interface ContextMenuPosition {
+ x: number;
+ y: number;
+}
-// const reducer = (state: DetailsPanelState = initialState, action: DetailsPanelAction) =>
-// actions.match(action, {
-// default: () => state,
-// LOAD_DETAILS: () => state,
-// LOAD_DETAILS_SUCCESS: ({ item }) => ({ ...state, item }),
-// TOGGLE_DETAILS_PANEL: () => ({ ...state, isOpened: !state.isOpened })
-// });
+export interface ContextMenuResource {
+ uuid: string;
+ kind: string;
+}
+
+const initialState = {
+ position: { x: 0, y: 0 }
+};
+
+export const contextMenuReducer = (state: ContextMenuState = initialState, action: ContextMenuAction) =>
+ contextMenuActions.match(action, {
+ default: () => state,
+ OPEN_CONTEXT_MENU: ({resource, position}) => ({ resource, position }),
+ CLOSE_CONTEXT_MENU: () => ({ position: state.position })
+ });
-// export default reducer;
import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
import { DataColumns } from "../../components/data-table/data-table";
-const actions = unionize({
+export const dataExplorerActions = unionize({
RESET_PAGINATION: ofType<{ id: string }>(),
REQUEST_ITEMS: ofType<{ id: string }>(),
SET_COLUMNS: ofType<{ id: string, columns: DataColumns<any> }>(),
SET_SEARCH_VALUE: ofType<{ id: string, searchValue: string }>(),
}, { tag: "type", value: "payload" });
-export type DataExplorerAction = UnionOf<typeof actions>;
-
-export default actions;
+export type DataExplorerAction = UnionOf<typeof dataExplorerActions>;
//
// SPDX-License-Identifier: AGPL-3.0
-import dataExplorerReducer, { initialDataExplorer } from "./data-explorer-reducer";
-import actions from "./data-explorer-action";
+import { dataExplorerReducer, initialDataExplorer } from "./data-explorer-reducer";
+import { dataExplorerActions } from "./data-explorer-action";
import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
import { DataColumns } from "../../components/data-table/data-table";
+import { SortDirection } from "../../components/data-table/data-column";
describe('data-explorer-reducer', () => {
it('should set columns', () => {
selected: true
}];
const state = dataExplorerReducer(undefined,
- actions.SET_COLUMNS({ id: "Data explorer", columns }));
+ dataExplorerActions.SET_COLUMNS({ id: "Data explorer", columns }));
expect(state["Data explorer"].columns).toEqual(columns);
});
name: "Column 1",
render: jest.fn(),
selected: true,
- sortDirection: "asc"
+ sortDirection: SortDirection.Asc
}, {
name: "Column 2",
render: jest.fn(),
selected: true,
- sortDirection: "none",
+ sortDirection: SortDirection.None,
}];
const state = dataExplorerReducer({ "Data explorer": { ...initialDataExplorer, columns } },
- actions.TOGGLE_SORT({ id: "Data explorer", columnName: "Column 2" }));
+ dataExplorerActions.TOGGLE_SORT({ id: "Data explorer", columnName: "Column 2" }));
expect(state["Data explorer"].columns[0].sortDirection).toEqual("none");
expect(state["Data explorer"].columns[1].sortDirection).toEqual("asc");
});
selected: true
}];
const state = dataExplorerReducer({ "Data explorer": { ...initialDataExplorer, columns } },
- actions.SET_FILTERS({ id: "Data explorer", columnName: "Column 1", filters }));
+ dataExplorerActions.SET_FILTERS({ id: "Data explorer", columnName: "Column 1", filters }));
expect(state["Data explorer"].columns[0].filters).toEqual(filters);
});
it('should set items', () => {
const state = dataExplorerReducer({ "Data explorer": undefined },
- actions.SET_ITEMS({ id: "Data explorer", items: ["Item 1", "Item 2"] }));
+ dataExplorerActions.SET_ITEMS({
+ id: "Data explorer",
+ items: ["Item 1", "Item 2"],
+ page: 0,
+ rowsPerPage: 10,
+ itemsAvailable: 100
+ }));
expect(state["Data explorer"].items).toEqual(["Item 1", "Item 2"]);
});
it('should set page', () => {
const state = dataExplorerReducer({ "Data explorer": undefined },
- actions.SET_PAGE({ id: "Data explorer", page: 2 }));
+ 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 },
- actions.SET_ROWS_PER_PAGE({ id: "Data explorer", rowsPerPage: 5 }));
+ 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 actions, { DataExplorerAction } from "./data-explorer-action";
+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";
export type DataExplorerState = Record<string, DataExplorer | undefined>;
-const dataExplorerReducer = (state: DataExplorerState = {}, action: DataExplorerAction) =>
- actions.match(action, {
+export const dataExplorerReducer = (state: DataExplorerState = {}, action: DataExplorerAction) =>
+ dataExplorerActions.match(action, {
RESET_PAGINATION: ({ id }) =>
update(state, id, explorer => ({ ...explorer, page: 0 })),
default: () => state
});
-export default dataExplorerReducer;
-
export const getDataExplorer = (state: DataExplorerState, id: string) =>
state[id] || initialDataExplorer;
// SPDX-License-Identifier: AGPL-3.0
import { unionize, ofType, UnionOf } from "unionize";
-import CommonResourceService from "../../common/api/common-resource-service";
+import { CommonResourceService } from "../../common/api/common-resource-service";
import { Dispatch } from "redux";
import { serverApi } from "../../common/api/server-api";
import { Resource, ResourceKind } from "../../models/resource";
-const actions = unionize({
+export const detailsPanelActions = unionize({
TOGGLE_DETAILS_PANEL: ofType<{}>(),
LOAD_DETAILS: ofType<{ uuid: string, kind: ResourceKind }>(),
LOAD_DETAILS_SUCCESS: ofType<{ item: Resource }>(),
}, { tag: 'type', value: 'payload' });
-export default actions;
-
-export type DetailsPanelAction = UnionOf<typeof actions>;
+export type DetailsPanelAction = UnionOf<typeof detailsPanelActions>;
export const loadDetails = (uuid: string, kind: ResourceKind) =>
(dispatch: Dispatch) => {
- dispatch(actions.LOAD_DETAILS({ uuid, kind }));
+ dispatch(detailsPanelActions.LOAD_DETAILS({ uuid, kind }));
getService(kind)
.get(uuid)
.then(project => {
- dispatch(actions.LOAD_DETAILS_SUCCESS({ item: project }));
+ dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item: project }));
});
};
//
// SPDX-License-Identifier: AGPL-3.0
-import actions, { DetailsPanelAction } from "./details-panel-action";
+import { detailsPanelActions, DetailsPanelAction } from "./details-panel-action";
import { Resource } from "../../models/resource";
export interface DetailsPanelState {
isOpened: false
};
-const reducer = (state: DetailsPanelState = initialState, action: DetailsPanelAction) =>
- actions.match(action, {
+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 })
});
-
-export default reducer;
// SPDX-License-Identifier: AGPL-3.0
import { Dispatch } from "redux";
-import projectActions, { getProjectList } from "../project/project-action";
+import { projectActions, getProjectList } from "../project/project-action";
import { push } from "react-router-redux";
import { TreeItemStatus } from "../../components/tree/tree";
import { findTreeItem } from "../project/project-reducer";
-import dataExplorerActions from "../data-explorer/data-explorer-action";
+import { dataExplorerActions } from "../data-explorer/data-explorer-action";
import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
import { RootState } from "../store";
import { Resource, ResourceKind } from "../../models/resource";
// SPDX-License-Identifier: AGPL-3.0
import { Middleware } from "redux";
-import actions from "../../store/data-explorer/data-explorer-action";
+import { dataExplorerActions } from "../data-explorer/data-explorer-action";
import { PROJECT_PANEL_ID, columns, ProjectPanelFilter, ProjectPanelColumnNames } from "../../views/project-panel/project-panel";
import { groupsService } from "../../services/services";
-import { RootState } from "../../store/store";
-import { getDataExplorer, DataExplorerState } from "../../store/data-explorer/data-explorer-reducer";
+import { RootState } from "../store";
+import { getDataExplorer } from "../data-explorer/data-explorer-reducer";
import { resourceToDataItem, ProjectPanelItem } from "../../views/project-panel/project-panel-item";
-import FilterBuilder from "../../common/api/filter-builder";
+import { FilterBuilder } from "../../common/api/filter-builder";
import { DataColumns } from "../../components/data-table/data-table";
import { ProcessResource } from "../../models/process";
-import { CollectionResource } from "../../models/collection";
-import OrderBuilder from "../../common/api/order-builder";
+import { OrderBuilder } from "../../common/api/order-builder";
import { GroupContentsResource, GroupContentsResourcePrefix } from "../../services/groups-service/groups-service";
import { SortDirection } from "../../components/data-table/data-column";
export const projectPanelMiddleware: Middleware = store => next => {
- next(actions.SET_COLUMNS({ id: PROJECT_PANEL_ID, columns }));
+ next(dataExplorerActions.SET_COLUMNS({ id: PROJECT_PANEL_ID, columns }));
return action => {
}
};
- actions.match(action, {
+ dataExplorerActions.match(action, {
SET_PAGE: handleProjectPanelAction(() => {
- store.dispatch(actions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+ store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
}),
SET_ROWS_PER_PAGE: handleProjectPanelAction(() => {
- store.dispatch(actions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+ store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
}),
SET_FILTERS: handleProjectPanelAction(() => {
- store.dispatch(actions.RESET_PAGINATION({ id: PROJECT_PANEL_ID }));
- store.dispatch(actions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+ store.dispatch(dataExplorerActions.RESET_PAGINATION({ id: PROJECT_PANEL_ID }));
+ store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
}),
TOGGLE_SORT: handleProjectPanelAction(() => {
- store.dispatch(actions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+ store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
}),
SET_SEARCH_VALUE: handleProjectPanelAction(() => {
- store.dispatch(actions.RESET_PAGINATION({ id: PROJECT_PANEL_ID }));
- store.dispatch(actions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+ store.dispatch(dataExplorerActions.RESET_PAGINATION({ id: PROJECT_PANEL_ID }));
+ store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
}),
REQUEST_ITEMS: handleProjectPanelAction(() => {
const state = store.getState() as RootState;
const dataExplorer = getDataExplorer(state.dataExplorer, PROJECT_PANEL_ID);
const columns = dataExplorer.columns as DataColumns<ProjectPanelItem, ProjectPanelFilter>;
- const typeFilters = getColumnFilters(columns, ProjectPanelColumnNames.Type);
- const statusFilters = getColumnFilters(columns, ProjectPanelColumnNames.Status);
+ 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) {
limit: dataExplorer.rowsPerPage,
offset: dataExplorer.page * dataExplorer.rowsPerPage,
order: sortColumn
- ? sortColumn.name === ProjectPanelColumnNames.Name
+ ? sortColumn.name === ProjectPanelColumnNames.NAME
? getOrder("name", sortDirection)
: getOrder("createdAt", sortDirection)
: OrderBuilder.create(),
.concat(getSearchFilter(dataExplorer.searchValue))
})
.then(response => {
- store.dispatch(actions.SET_ITEMS({
+ store.dispatch(dataExplorerActions.SET_ITEMS({
id: PROJECT_PANEL_ID,
items: response.items.map(resourceToDataItem),
itemsAvailable: response.itemsAvailable,
}));
});
} else {
- store.dispatch(actions.SET_ITEMS({
+ store.dispatch(dataExplorerActions.SET_ITEMS({
id: PROJECT_PANEL_ID,
items: [],
itemsAvailable: 0,
import { ProjectResource } from "../../models/project";
import { projectService } from "../../services/services";
import { Dispatch } from "redux";
-import FilterBuilder from "../../common/api/filter-builder";
+import { FilterBuilder } from "../../common/api/filter-builder";
import { RootState } from "../store";
-const actions = unionize({
+export const projectActions = unionize({
OPEN_PROJECT_CREATOR: ofType<{ ownerUuid: string }>(),
CLOSE_PROJECT_CREATOR: ofType<{}>(),
CREATE_PROJECT: ofType<Partial<ProjectResource>>(),
});
export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch) => {
- dispatch(actions.PROJECTS_REQUEST(parentUuid));
+ dispatch(projectActions.PROJECTS_REQUEST(parentUuid));
return projectService.list({
filters: FilterBuilder
.create<ProjectResource>()
.addEqual("ownerUuid", parentUuid)
}).then(({ items: projects }) => {
- dispatch(actions.PROJECTS_SUCCESS({ projects, parentItemId: parentUuid }));
+ dispatch(projectActions.PROJECTS_SUCCESS({ projects, parentItemId: parentUuid }));
return projects;
});
};
(dispatch: Dispatch, getState: () => RootState) => {
const { ownerUuid } = getState().projects.creator;
const projectData = { ownerUuid, ...project };
- dispatch(actions.CREATE_PROJECT(projectData));
+ dispatch(projectActions.CREATE_PROJECT(projectData));
return projectService
.create(projectData)
- .then(project => dispatch(actions.CREATE_PROJECT_SUCCESS(project)));
+ .then(project => dispatch(projectActions.CREATE_PROJECT_SUCCESS(project)));
};
-export type ProjectAction = UnionOf<typeof actions>;
-export default actions;
+export type ProjectAction = UnionOf<typeof projectActions>;
//
// SPDX-License-Identifier: AGPL-3.0
-import projectsReducer, { getTreePath } from "./project-reducer";
-import actions from "./project-action";
+import { projectsReducer, getTreePath } from "./project-reducer";
+import { projectActions } from "./project-action";
import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
import { mockProjectResource } from "../../models/test-utils";
const initialState = undefined;
const projects = [mockProjectResource({ uuid: "1" }), mockProjectResource({ uuid: "2" })];
- const state = projectsReducer(initialState, actions.PROJECTS_SUCCESS({ projects, parentItemId: undefined }));
+ const state = projectsReducer(initialState, projectActions.PROJECTS_SUCCESS({ projects, parentItemId: undefined }));
expect(state).toEqual({
items: [{
active: false,
creator: { opened: false, pending: false, ownerUuid: "" },
};
- const state = projectsReducer(initialState, actions.RESET_PROJECT_TREE_ACTIVITY(initialState.items[0].id));
+ const state = projectsReducer(initialState, projectActions.RESET_PROJECT_TREE_ACTIVITY(initialState.items[0].id));
expect(state).toEqual(project);
});
creator: { opened: false, pending: false, ownerUuid: "" },
};
- const state = projectsReducer(initialState, actions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(initialState.items[0].id));
+ const state = projectsReducer(initialState, projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(initialState.items[0].id));
expect(state).toEqual(project);
});
creator: { opened: false, pending: false, ownerUuid: "" },
};
- const state = projectsReducer(initialState, actions.TOGGLE_PROJECT_TREE_ITEM_OPEN(initialState.items[0].id));
+ const state = projectsReducer(initialState, projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(initialState.items[0].id));
expect(state).toEqual(project);
});
});
import * as _ from "lodash";
-import actions, { ProjectAction } from "./project-action";
+import { projectActions, ProjectAction } from "./project-action";
import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
import { ProjectResource } from "../../models/project";
};
-const projectsReducer = (state: ProjectState = initialState, action: ProjectAction) => {
- return actions.match(action, {
- OPEN_PROJECT_CREATOR: ({ ownerUuid }) => updateCreator(state, { ownerUuid, opened: true }),
+export const projectsReducer = (state: ProjectState = initialState, action: ProjectAction) => {
+ return projectActions.match(action, {
+ OPEN_PROJECT_CREATOR: ({ ownerUuid }) => updateCreator(state, { ownerUuid, opened: true, pending: false }),
CLOSE_PROJECT_CREATOR: () => updateCreator(state, { opened: false }),
CREATE_PROJECT: () => updateCreator(state, { error: undefined }),
CREATE_PROJECT_SUCCESS: () => updateCreator(state, { opened: false, ownerUuid: "" }),
default: () => state
});
};
-
-export default projectsReducer;
import { default as unionize, ofType, UnionOf } from "unionize";
-const actions = unionize({
+export const sidePanelActions = unionize({
TOGGLE_SIDE_PANEL_ITEM_OPEN: ofType<string>(),
TOGGLE_SIDE_PANEL_ITEM_ACTIVE: ofType<string>(),
RESET_SIDE_PANEL_ACTIVITY: ofType<{}>(),
value: 'payload'
});
-export type SidePanelAction = UnionOf<typeof actions>;
-export default actions;
\ No newline at end of file
+export type SidePanelAction = UnionOf<typeof sidePanelActions>;
//
// SPDX-License-Identifier: AGPL-3.0
-import sidePanelReducer from "./side-panel-reducer";
-import actions from "./side-panel-action";
+import { sidePanelReducer } from "./side-panel-reducer";
+import { sidePanelActions } from "./side-panel-action";
+import { ProjectsIcon } from "../../components/icon/icon";
describe('side-panel-reducer', () => {
{
id: "1",
name: "Projects",
- icon: "fas fa-th fa-fw",
+ icon: ProjectsIcon,
open: false,
active: false,
}
{
id: "1",
name: "Projects",
- icon: "fas fa-th fa-fw",
+ icon: ProjectsIcon,
open: false,
active: true,
}
];
- const state = sidePanelReducer(initialState, actions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(initialState[0].id));
+ const state = sidePanelReducer(initialState, sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(initialState[0].id));
expect(state).toEqual(project);
});
{
id: "1",
name: "Projects",
- icon: "fas fa-th fa-fw",
+ icon: ProjectsIcon,
open: false,
active: false,
}
{
id: "1",
name: "Projects",
- icon: "fas fa-th fa-fw",
+ icon: ProjectsIcon,
open: true,
active: false,
}
];
- const state = sidePanelReducer(initialState, actions.TOGGLE_SIDE_PANEL_ITEM_OPEN(initialState[0].id));
+ const state = sidePanelReducer(initialState, sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(initialState[0].id));
expect(state).toEqual(project);
});
{
id: "1",
name: "Projects",
- icon: "fas fa-th fa-fw",
+ icon: ProjectsIcon,
open: false,
active: true,
}
{
id: "1",
name: "Projects",
- icon: "fas fa-th fa-fw",
+ icon: ProjectsIcon,
open: false,
active: false,
}
];
- const state = sidePanelReducer(initialState, actions.RESET_SIDE_PANEL_ACTIVITY(initialState[0].id));
+ const state = sidePanelReducer(initialState, sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(initialState[0].id));
expect(state).toEqual(project);
});
-});
\ No newline at end of file
+});
// SPDX-License-Identifier: AGPL-3.0
import * as _ from "lodash";
-
-import actions, { SidePanelAction } from './side-panel-action';
+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";
export type SidePanelState = SidePanelItem[];
-const sidePanelReducer = (state: SidePanelState = sidePanelData, action: SidePanelAction) => {
+export const sidePanelReducer = (state: SidePanelState = sidePanelData, action: SidePanelAction) => {
if (state.length === 0) {
return sidePanelData;
} else {
- return actions.match(action, {
+ return sidePanelActions.match(action, {
TOGGLE_SIDE_PANEL_ITEM_OPEN: itemId => state.map(it => itemId === it.id && it.open === false ? {...it, open: true} : {...it, open: false}),
TOGGLE_SIDE_PANEL_ITEM_ACTIVE: itemId => {
const sidePanel = _.cloneDeep(state);
{
id: SidePanelIdentifiers.Projects,
name: "Projects",
- icon: "fas fa-th fa-fw",
+ icon: ProjectsIcon,
open: false,
active: false,
margin: true,
{
id: SidePanelIdentifiers.SharedWithMe,
name: "Shared with me",
- icon: "fas fa-users fa-fw",
+ icon: ShareMeIcon,
active: false,
},
{
id: SidePanelIdentifiers.Workflows,
name: "Workflows",
- icon: "fas fa-cogs fa-fw",
+ icon: WorkflowIcon,
active: false,
},
{
id: SidePanelIdentifiers.RecentOpen,
name: "Recent open",
- icon: "icon-time fa-fw",
+ icon: RecentIcon,
active: false,
},
{
id: SidePanelIdentifiers.Favourites,
name: "Favorites",
- icon: "fas fa-star fa-fw",
+ icon: FavoriteIcon,
active: false,
},
{
id: SidePanelIdentifiers.Trash,
name: "Trash",
- icon: "fas fa-trash-alt fa-fw",
+ icon: TrashIcon,
active: false,
}
];
t.active = false;
}
}
-
-export default sidePanelReducer;
import thunkMiddleware from 'redux-thunk';
import { History } from "history";
-import projectsReducer, { ProjectState } from "./project/project-reducer";
-import sidePanelReducer, { SidePanelState } from './side-panel/side-panel-reducer';
-import authReducer, { AuthState } from "./auth/auth-reducer";
-import dataExplorerReducer, { DataExplorerState } from './data-explorer/data-explorer-reducer';
-import { projectPanelMiddleware } from '../store/project-panel/project-panel-middleware';
-import detailsPanelReducer, { DetailsPanelState } from './details-panel/details-panel-reducer';
+import { projectsReducer, ProjectState } from "./project/project-reducer";
+import { sidePanelReducer, SidePanelState } from './side-panel/side-panel-reducer';
+import { authReducer, AuthState } from "./auth/auth-reducer";
+import { dataExplorerReducer, DataExplorerState } from './data-explorer/data-explorer-reducer';
+import { projectPanelMiddleware } from './project-panel/project-panel-middleware';
+import { detailsPanelReducer, DetailsPanelState } from './details-panel/details-panel-reducer';
+import { contextMenuReducer, ContextMenuState } from './context-menu/context-menu-reducer';
import { reducer as formReducer } from 'redux-form';
const composeEnhancers =
dataExplorer: DataExplorerState;
sidePanel: SidePanelState;
detailsPanel: DetailsPanelState;
+ contextMenu: ContextMenuState;
}
const rootReducer = combineReducers({
dataExplorer: dataExplorerReducer,
sidePanel: sidePanelReducer,
detailsPanel: detailsPanelReducer,
+ contextMenu: contextMenuReducer,
form: formReducer
});
-export default function configureStore(history: History) {
+export function configureStore(history: History) {
const middlewares: Middleware[] = [
routerMiddleware(history),
thunkMiddleware,
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+
+type CssRules = "formInputError";
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+ formInputError: {
+ color: "#ff0000",
+ marginLeft: "5px",
+ fontSize: "11px",
+ }
+});
+
+type ValidatorProps = {
+ value: string,
+ onChange: (isValid: boolean | string) => void;
+ render: (hasError: boolean) => React.ReactElement<any>;
+ isRequired: boolean;
+};
+
+interface ValidatorState {
+ isPatternValid: boolean;
+ isLengthValid: boolean;
+}
+
+const nameRegEx = /^[a-zA-Z0-9-_ ]+$/;
+const maxInputLength = 60;
+
+export const Validator = withStyles(styles)(
+ class extends React.Component<ValidatorProps & WithStyles<CssRules>> {
+ state: ValidatorState = {
+ isPatternValid: true,
+ isLengthValid: true
+ };
+
+ componentWillReceiveProps(nextProps: ValidatorProps) {
+ const { value } = nextProps;
+
+ if (this.props.value !== value) {
+ this.setState({
+ isPatternValid: value.match(nameRegEx),
+ isLengthValid: value.length < maxInputLength
+ }, () => this.onChange());
+ }
+ }
+
+ onChange() {
+ const { value, onChange, isRequired } = this.props;
+ const { isPatternValid, isLengthValid } = this.state;
+ const isValid = value && isPatternValid && isLengthValid && (isRequired || (!isRequired && value.length > 0));
+
+ onChange(isValid);
+ }
+
+ render() {
+ const { classes, isRequired, value } = this.props;
+ const { isPatternValid, isLengthValid } = this.state;
+
+ return (
+ <span>
+ {this.props.render(!(isPatternValid && isLengthValid) && (isRequired || (!isRequired && value.length > 0)))}
+ {!isPatternValid && (isRequired || (!isRequired && value.length > 0)) ?
+ <span className={classes.formInputError}>This field allow only alphanumeric characters, dashes, spaces and underscores.<br/></span> : null}
+ {!isLengthValid ?
+ <span className={classes.formInputError}>This field should have max 60 characters.</span> : null}
+ </span>
+ );
+ }
+ }
+);
import { Redirect, RouteProps } from "react-router";
import * as React from "react";
import { connect, DispatchProp } from "react-redux";
-import authActions, { getUserDetails } from "../../store/auth/auth-action";
+import { authActions, getUserDetails } from "../../store/auth/auth-action";
import { authService } from "../../services/services";
import { getProjectList } from "../../store/project/project-action";
+import { getUrlParameter } from "../../common/url";
interface ApiTokenProps {
}
-class ApiToken extends React.Component<ApiTokenProps & RouteProps & DispatchProp<any>, {}> {
- static getUrlParameter(search: string, name: string) {
- const safeName = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
- const regex = new RegExp('[\\?&]' + safeName + '=([^&#]*)');
- const results = regex.exec(search);
- return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
+export const ApiToken = connect()(
+ class extends React.Component<ApiTokenProps & RouteProps & DispatchProp<any>, {}> {
+ componentDidMount() {
+ const search = this.props.location ? this.props.location.search : "";
+ const apiToken = getUrlParameter(search, 'api_token');
+ this.props.dispatch(authActions.SAVE_API_TOKEN(apiToken));
+ this.props.dispatch<any>(getUserDetails()).then(() => {
+ const rootUuid = authService.getRootUuid();
+ this.props.dispatch(getProjectList(rootUuid));
+ });
+ }
+ render() {
+ return <Redirect to="/"/>;
+ }
}
-
- componentDidMount() {
- const search = this.props.location ? this.props.location.search : "";
- const apiToken = ApiToken.getUrlParameter(search, 'api_token');
- this.props.dispatch(authActions.SAVE_API_TOKEN(apiToken));
- this.props.dispatch<any>(getUserDetails()).then(() => {
- const rootUuid = authService.getRootUuid();
- this.props.dispatch(getProjectList(rootUuid));
- });
- }
- render() {
- return <Redirect to="/"/>;
- }
-}
-
-export default connect()(ApiToken);
+);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import { projectActions } from "../../../store/project/project-action";
+import { ShareIcon, NewProjectIcon } from "../../../components/icon/icon";
+
+export const projectActionSet: ContextMenuActionSet = [[{
+ icon: NewProjectIcon,
+ name: "New project",
+ execute: (dispatch, resource) => {
+ dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid }));
+ }
+}, {
+ icon: ShareIcon,
+ name: "Share",
+ execute: () => { return; }
+}]];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import { projectActions } from "../../../store/project/project-action";
+import { NewProjectIcon } from "../../../components/icon/icon";
+
+export const rootProjectActionSet: ContextMenuActionSet = [[{
+ icon: NewProjectIcon,
+ name: "New project",
+ execute: (dispatch, resource) => {
+ dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid }));
+ }
+}]];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// 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";
+
+export interface ContextMenuAction extends ContextMenuItem {
+ execute(dispatch: Dispatch, resource: ContextMenuResource): void;
+}
+
+export type ContextMenuActionSet = Array<Array<ContextMenuAction>>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// 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 { ContextMenuActionSet, ContextMenuAction } from "./context-menu-action-set";
+import { Dispatch } from "redux";
+
+type DataProps = Pick<ContextMenuProps, "anchorEl" | "items"> & { resource?: ContextMenuResource };
+const mapStateToProps = (state: RootState): DataProps => {
+ const { position, resource } = state.contextMenu;
+ return {
+ anchorEl: resource ? createAnchorAt(position) : undefined,
+ items: getMenuActionSet(resource),
+ resource
+ };
+};
+
+type ActionProps = Pick<ContextMenuProps, "onClose"> & { onItemClick: (item: ContextMenuItem, resource?: ContextMenuResource) => void };
+const mapDispatchToProps = (dispatch: Dispatch): ActionProps => ({
+ onClose: () => {
+ dispatch(contextMenuActions.CLOSE_CONTEXT_MENU());
+ },
+ onItemClick: (action: ContextMenuAction, resource?: ContextMenuResource) => {
+ dispatch(contextMenuActions.CLOSE_CONTEXT_MENU());
+ if (resource) {
+ action.execute(dispatch, resource);
+ }
+ }
+});
+
+const mergeProps = ({ resource, ...dataProps }: DataProps, actionProps: ActionProps): ContextMenuProps => ({
+ ...dataProps,
+ ...actionProps,
+ onItemClick: item => {
+ actionProps.onItemClick(item, resource);
+ }
+});
+
+export const ContextMenu = connect(mapStateToProps, mapDispatchToProps, mergeProps)(ContextMenuComponent);
+
+const menuActionSets = new Map<string, ContextMenuActionSet>();
+
+export const addMenuActionSet = (name: string, itemSet: ContextMenuActionSet) => {
+ menuActionSets.set(name, itemSet);
+};
+
+const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet => {
+ return resource ? menuActionSets.get(resource.kind) || [] : [];
+};
+
+export enum ContextMenuKind {
+ RootProject = "RootProject",
+ Project = "Project"
+}
import { SubmissionError } from "redux-form";
import { RootState } from "../../store/store";
-import DialogProjectCreate from "../dialog-create/dialog-project-create";
-import actions, { createProject, getProjectList } from "../../store/project/project-action";
-import dataExplorerActions from "../../store/data-explorer/data-explorer-action";
+import DialogProjectCreate from "../dialog-create/dialog-project-create";
+import { projectActions, createProject, getProjectList } from "../../store/project/project-action";
+import { dataExplorerActions } from "../../store/data-explorer/data-explorer-action";
import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
const mapStateToProps = (state: RootState) => ({
const mapDispatchToProps = (dispatch: Dispatch) => ({
handleClose: () => {
- dispatch(actions.CLOSE_PROJECT_CREATOR());
+ dispatch(projectActions.CLOSE_PROJECT_CREATOR());
},
onSubmit: (data: { name: string, description: string }) => {
return dispatch<any>(addProject(data))
}
});
-export default connect(mapStateToProps, mapDispatchToProps)(DialogProjectCreate);
+export const CreateProjectDialog = connect(mapStateToProps, mapDispatchToProps)(DialogProjectCreate);
import { connect } from "react-redux";
import { RootState } from "../../store/store";
-import DataExplorer from "../../components/data-explorer/data-explorer";
+import { DataExplorer as DataExplorerComponent } from "../../components/data-explorer/data-explorer";
import { getDataExplorer } from "../../store/data-explorer/data-explorer-reducer";
import { Dispatch } from "redux";
-import actions from "../../store/data-explorer/data-explorer-action";
+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";
const mapDispatchToProps = (dispatch: Dispatch, { id, onRowClick, onRowDoubleClick, onContextMenu }: Props) => ({
onSearch: (searchValue: string) => {
- dispatch(actions.SET_SEARCH_VALUE({ id, searchValue }));
+ dispatch(dataExplorerActions.SET_SEARCH_VALUE({ id, searchValue }));
},
onColumnToggle: (column: DataColumn<any>) => {
- dispatch(actions.TOGGLE_COLUMN({ id, columnName: column.name }));
+ dispatch(dataExplorerActions.TOGGLE_COLUMN({ id, columnName: column.name }));
},
onSortToggle: (column: DataColumn<any>) => {
- dispatch(actions.TOGGLE_SORT({ id, columnName: column.name }));
+ dispatch(dataExplorerActions.TOGGLE_SORT({ id, columnName: column.name }));
},
onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<any>) => {
- dispatch(actions.SET_FILTERS({ id, columnName: column.name, filters }));
+ dispatch(dataExplorerActions.SET_FILTERS({ id, columnName: column.name, filters }));
},
onChangePage: (page: number) => {
- dispatch(actions.SET_PAGE({ id, page }));
+ dispatch(dataExplorerActions.SET_PAGE({ id, page }));
},
onChangeRowsPerPage: (rowsPerPage: number) => {
- dispatch(actions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
+ dispatch(dataExplorerActions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
},
onRowClick,
onRowDoubleClick,
-
+
onContextMenu,
});
-export default connect(mapStateToProps, mapDispatchToProps)(DataExplorer);
+export const DataExplorer = connect(mapStateToProps, mapDispatchToProps)(DataExplorerComponent);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// 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 { DetailsData } from "./details-data";
+import { DetailsAttribute } from "../../components/details-attribute/details-attribute";
+
+export class CollectionDetails extends DetailsData<CollectionResource> {
+
+ getIcon(className?: string) {
+ return <CollectionIcon className={className} />;
+ }
+
+ getDetails() {
+ return <div>
+ <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.Collection)} />
+ <DetailsAttribute label='Size' value='---' />
+ <DetailsAttribute label='Owner' value={this.item.ownerUuid} />
+ <DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
+ <DetailsAttribute label='Created at' value={formatDate(this.item.createdAt)} />
+ {/* Links but we dont have view */}
+ <DetailsAttribute label='Collection UUID' link={this.item.uuid} value={this.item.uuid} />
+ <DetailsAttribute label='Content address' link={this.item.portableDataHash} value={this.item.portableDataHash} />
+ {/* Missing attrs */}
+ <DetailsAttribute label='Number of files' value='20' />
+ <DetailsAttribute label='Content size' value='54 MB' />
+ </div>;
+ }
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { DetailsResource } from "../../models/details";
+
+export abstract class DetailsData<T extends DetailsResource = DetailsResource> {
+ constructor(protected item: T) {}
+
+ getTitle(): string {
+ return this.item.name;
+ }
+
+ abstract getIcon(className?: string): React.ReactElement<any>;
+ abstract getDetails(): React.ReactElement<any>;
+
+ getActivity(): React.ReactElement<any> {
+ return <div/>;
+ }
+}
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import Drawer from '@material-ui/core/Drawer';
-import IconButton from "@material-ui/core/IconButton";
+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 Tabs from '@material-ui/core/Tabs';
-import Tab from '@material-ui/core/Tab';
-import Typography from '@material-ui/core/Typography';
-import Grid from '@material-ui/core/Grid';
import * as classnames from "classnames";
-import { connect, Dispatch } from 'react-redux';
+import { connect } from 'react-redux';
import { RootState } from '../../store/store';
-import actions from "../../store/details-panel/details-panel-action";
-import { ProjectResource } from '../../models/project';
-import { CollectionResource } from '../../models/collection';
-import IconBase, { IconTypes } from '../../components/icon/icon';
-import { ProcessResource } from '../../models/process';
-import DetailsPanelFactory from '../../components/details-panel-factory/details-panel-factory';
-import AbstractItem from '../../components/details-panel-factory/items/abstract-item';
+import { detailsPanelActions } from "../../store/details-panel/details-panel-action";
+import { CloseIcon } from '../../components/icon/icon';
import { EmptyResource } from '../../models/empty';
-
-export interface DetailsPanelDataProps {
- onCloseDrawer: () => void;
- isOpened: boolean;
- item: AbstractItem;
-}
-
-type DetailsPanelProps = DetailsPanelDataProps & WithStyles<CssRules>;
-
-class DetailsPanel extends React.Component<DetailsPanelProps, {}> {
- state = {
- tabsValue: 0
- };
-
- handleChange = (event: any, value: boolean) => {
- this.setState({ tabsValue: value });
- }
-
- renderTabContainer = (children: React.ReactElement<any>) =>
- <Typography className={this.props.classes.tabContainer} component="div">
- {children}
- </Typography>
-
- render() {
- const { classes, onCloseDrawer, isOpened, item } = this.props;
- const { tabsValue } = this.state;
- return (
- <Typography component="div" className={classnames([classes.container, { [classes.opened]: isOpened }])}>
- <Drawer variant="permanent" anchor="right" classes={{ paper: classes.drawerPaper }}>
- <Typography component="div" className={classes.headerContainer}>
- <Grid container alignItems='center' justify='space-around'>
- <Grid item xs={2}>
- <IconBase className={classes.headerIcon} icon={item.getIcon()} />
- </Grid>
- <Grid item xs={8}>
- <Typography variant="title">
- {item.getTitle()}
- </Typography>
- </Grid>
- <Grid item>
- <IconButton color="inherit" onClick={onCloseDrawer}>
- <IconBase icon={IconTypes.CLOSE} />
- </IconButton>
- </Grid>
- </Grid>
- </Typography>
- <Tabs value={tabsValue} onChange={this.handleChange}>
- <Tab disableRipple label="Details" />
- <Tab disableRipple label="Activity" disabled />
- </Tabs>
- {tabsValue === 0 && this.renderTabContainer(
- <Grid container direction="column">
- {item.buildDetails()}
- </Grid>
- )}
- {tabsValue === 1 && this.renderTabContainer(
- <Grid container direction="column" />
- )}
- </Drawer>
- </Typography>
- );
- }
-
-}
+import { Dispatch } from "redux";
+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";
type CssRules = 'drawerPaper' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'tabContainer';
}
});
-
-export type DetailsPanelResource = ProjectResource | CollectionResource | ProcessResource | EmptyResource;
-
-const getItem = (res: DetailsPanelResource) => {
- return DetailsPanelFactory.createItem(res);
-};
-
-const getDefaultItem = () => {
- return DetailsPanelFactory.createItem({ kind: undefined, name: 'Projects' });
+const getItem = (resource: DetailsResource): DetailsData => {
+ const res = resource || { kind: undefined, name: 'Projects' };
+ switch (res.kind) {
+ case ResourceKind.Project:
+ return new ProjectDetails(res);
+ case ResourceKind.Collection:
+ return new CollectionDetails(res);
+ case ResourceKind.Process:
+ return new ProcessDetails(res);
+ default:
+ return new EmptyDetails(res as EmptyResource);
+ }
};
-const mapStateToProps = ({ detailsPanel }: RootState) => {
- const { isOpened, item } = detailsPanel;
- return {
- isOpened,
- item: item ? getItem(item as DetailsPanelResource) : getDefaultItem()
- };
-};
+const mapStateToProps = ({ detailsPanel }: RootState) => ({
+ isOpened: detailsPanel.isOpened,
+ item: getItem(detailsPanel.item as DetailsResource)
+});
const mapDispatchToProps = (dispatch: Dispatch) => ({
onCloseDrawer: () => {
- dispatch(actions.TOGGLE_DETAILS_PANEL());
+ dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
}
});
-const DetailsPanelContainer = connect(mapStateToProps, mapDispatchToProps)(DetailsPanel);
+export interface DetailsPanelDataProps {
+ onCloseDrawer: () => void;
+ isOpened: boolean;
+ item: DetailsData;
+}
-export default withStyles(styles)(DetailsPanelContainer);
\ No newline at end of file
+type DetailsPanelProps = DetailsPanelDataProps & WithStyles<CssRules>;
+
+export const DetailsPanel = withStyles(styles)(
+ connect(mapStateToProps, mapDispatchToProps)(
+ class extends React.Component<DetailsPanelProps> {
+ state = {
+ tabsValue: 0
+ };
+
+ handleChange = (event: any, value: boolean) => {
+ this.setState({ tabsValue: value });
+ }
+
+ renderTabContainer = (children: React.ReactElement<any>) =>
+ <Typography className={this.props.classes.tabContainer} component="div">
+ {children}
+ </Typography>
+
+ render() {
+ const { classes, onCloseDrawer, isOpened, item } = this.props;
+ const { tabsValue } = this.state;
+ return (
+ <Typography component="div"
+ className={classnames([classes.container, { [classes.opened]: isOpened }])}>
+ <Drawer variant="permanent" anchor="right" classes={{ paper: classes.drawerPaper }}>
+ <Typography component="div" className={classes.headerContainer}>
+ <Grid container alignItems='center' justify='space-around'>
+ <Grid item xs={2}>
+ {item.getIcon(classes.headerIcon)}
+ </Grid>
+ <Grid item xs={8}>
+ <Typography variant="title">
+ {item.getTitle()}
+ </Typography>
+ </Grid>
+ <Grid item>
+ <IconButton color="inherit" onClick={onCloseDrawer}>
+ {<CloseIcon/>}
+ </IconButton>
+ </Grid>
+ </Grid>
+ </Typography>
+ <Tabs value={tabsValue} onChange={this.handleChange}>
+ <Tab disableRipple label="Details"/>
+ <Tab disableRipple label="Activity" disabled/>
+ </Tabs>
+ {tabsValue === 0 && this.renderTabContainer(
+ <Grid container direction="column">
+ {item.getDetails()}
+ </Grid>
+ )}
+ {tabsValue === 1 && this.renderTabContainer(
+ <Grid container direction="column"/>
+ )}
+ </Drawer>
+ </Typography>
+ );
+ }
+ }
+ )
+);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { DefaultIcon, IconType, ProjectsIcon } from '../../components/icon/icon';
+import { EmptyResource } from '../../models/empty';
+import { DetailsData } from "./details-data";
+import Typography from "@material-ui/core/Typography";
+import { StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core/styles";
+import { ArvadosTheme } from "../../common/custom-theme";
+import Icon from "@material-ui/core/Icon/Icon";
+
+type CssRules = 'container' | 'icon';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ container: {
+ textAlign: 'center'
+ },
+ icon: {
+ color: theme.palette.grey["500"],
+ fontSize: '72px'
+ }
+});
+
+export interface EmptyStateDataProps {
+ message: string;
+ icon: IconType;
+ details?: string;
+ children?: React.ReactNode;
+}
+
+type EmptyStateProps = EmptyStateDataProps & WithStyles<CssRules>;
+
+const EmptyState = withStyles(styles)(
+ ({ classes, details, message, children, icon: Icon }: EmptyStateProps) =>
+ <Typography className={classes.container} component="div">
+ <Icon className={classes.icon}/>
+ <Typography variant="body1" gutterBottom>{message}</Typography>
+ {details && <Typography gutterBottom>{details}</Typography>}
+ {children && <Typography gutterBottom>{children}</Typography>}
+ </Typography>
+);
+
+export class EmptyDetails extends DetailsData<EmptyResource> {
+ getIcon(className?: string) {
+ return <ProjectsIcon className={className}/>;
+ }
+
+ getDetails() {
+ return <EmptyState icon={DefaultIcon} message='Select a file or folder to view its details.'/>;
+ }
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// 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 { DetailsData } from "./details-data";
+import { DetailsAttribute } from "../../components/details-attribute/details-attribute";
+
+export class ProcessDetails extends DetailsData<ProcessResource> {
+
+ getIcon(className?: string){
+ return <ProcessIcon className={className} />;
+ }
+
+ getDetails() {
+ return <div>
+ <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.Process)} />
+ <DetailsAttribute label='Size' value='---' />
+ <DetailsAttribute label='Owner' value={this.item.ownerUuid} />
+
+ {/* Missing attr */}
+ <DetailsAttribute label='Status' value={this.item.state} />
+ <DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
+
+ {/* Missing attrs */}
+ <DetailsAttribute label='Started at' value={formatDate(this.item.createdAt)} />
+ <DetailsAttribute label='Finished at' value={formatDate(this.item.expiresAt)} />
+
+ {/* Links but we dont have view */}
+ <DetailsAttribute label='Outputs' link={this.item.outputPath} value={this.item.outputPath} />
+ <DetailsAttribute label='UUID' link={this.item.uuid} value={this.item.uuid} />
+ <DetailsAttribute label='Container UUID' link={this.item.containerUuid} value={this.item.containerUuid} />
+
+ <DetailsAttribute label='Priority' value={this.item.priority} />
+ <DetailsAttribute label='Runtime Constraints' value={this.item.runtimeConstraints} />
+ {/* Link but we dont have view */}
+ <DetailsAttribute label='Docker Image locator' link={this.item.containerImage} value={this.item.containerImage} />
+ </div>;
+ }
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// 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 { DetailsData } from "./details-data";
+import { DetailsAttribute } from "../../components/details-attribute/details-attribute";
+
+export class ProjectDetails extends DetailsData<ProjectResource> {
+
+ getIcon(className?: string) {
+ return <ProjectIcon className={className} />;
+ }
+
+ getDetails() {
+ return <div>
+ <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.Project)} />
+ {/* Missing attr */}
+ <DetailsAttribute label='Size' value='---' />
+ <DetailsAttribute label='Owner' value={this.item.ownerUuid} />
+ <DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
+ <DetailsAttribute label='Created at' value={formatDate(this.item.createdAt)} />
+ {/* Missing attr */}
+ <DetailsAttribute label='File size' value='1.4 GB' />
+ <DetailsAttribute label='Description' value={this.item.description} />
+ </div>;
+ }
+}
export default compose(
reduxForm({ form: 'projectCreateDialog' }),
withStyles(styles)
-)(DialogProjectCreate);
\ No newline at end of file
+)(DialogProjectCreate);
import * as React from "react";
import { mount, configure, ReactWrapper } 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 { 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 { Button, MenuItem, IconButton } from "@material-ui/core";
import { User } from "../../models/user";
const mainAppBar = mount(
<MainAppBar
user={user}
+ onContextMenu={jest.fn()}
onDetailsPanelToggle={jest.fn()}
onContextMenu={jest.fn()}
{...{ searchText: "", breadcrumbs: [], menuItems: { accountMenu: [], helpMenu: [], anonymousMenu: [] }, onSearch: jest.fn(), onBreadcrumbClick: jest.fn(), onMenuItemClick: jest.fn() }}
<MainAppBar
searchText="search text"
searchDebounce={2000}
+ onContextMenu={jest.fn()}
onSearch={onSearch}
onContextMenu={jest.fn()}
onDetailsPanelToggle={jest.fn()}
const mainAppBar = mount(
<MainAppBar
breadcrumbs={items}
+ onContextMenu={jest.fn()}
onBreadcrumbClick={onBreadcrumbClick}
onContextMenu={jest.fn()}
onDetailsPanelToggle={jest.fn()}
const mainAppBar = mount(
<MainAppBar
menuItems={menuItems}
+ onContextMenu={jest.fn()}
onMenuItemClick={onMenuItemClick}
onContextMenu={jest.fn()}
onDetailsPanelToggle={jest.fn()}
import * as React from "react";
import { AppBar, Toolbar, Typography, Grid, IconButton, Badge, Button, MenuItem } from "@material-ui/core";
-import NotificationsIcon from "@material-ui/icons/Notifications";
-import PersonIcon from "@material-ui/icons/Person";
-import HelpIcon from "@material-ui/icons/Help";
-import InfoIcon from '@material-ui/icons/Info';
-import SearchBar from "../../components/search-bar/search-bar";
-import Breadcrumbs, { Breadcrumb } from "../../components/breadcrumbs/breadcrumbs";
-import DropdownMenu from "../../components/dropdown-menu/dropdown-menu";
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;
onClick={props.onBreadcrumbClick}
onContextMenu={props.onContextMenu} />
}
- <IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
- <InfoIcon />
- </IconButton>
+ { props.user && <IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
+ <DetailsIcon />
+ </IconButton>
+ }
</Toolbar>
</AppBar>;
};
-
const renderMenuForUser = ({ user, menuItems, onMenuItemClick }: MainAppBarProps) => {
return (
<>
<IconButton color="inherit">
<Badge badgeContent={3} color="primary">
- <NotificationsIcon />
+ <NotificationIcon />
</Badge>
</IconButton>
- <DropdownMenu icon={PersonIcon} id="account-menu">
+ <DropdownMenu icon={<UserPanelIcon />} id="account-menu">
<MenuItem>
{getUserFullname(user)}
</MenuItem>
{renderMenuItems(menuItems.accountMenu, onMenuItemClick)}
</DropdownMenu>
- <DropdownMenu icon={HelpIcon} id="help-menu">
+ <DropdownMenu icon={<HelpIcon />} id="help-menu">
{renderMenuItems(menuItems.helpMenu, onMenuItemClick)}
</DropdownMenu>
</>
</MenuItem>
));
};
-
-export default MainAppBar;
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import { Theme } from "@material-ui/core";
-import { StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core/styles";
-import Paper from "@material-ui/core/Paper/Paper";
-import Table from "@material-ui/core/Table/Table";
-import TableHead from "@material-ui/core/TableHead/TableHead";
-import TableRow from "@material-ui/core/TableRow/TableRow";
-import TableCell from "@material-ui/core/TableCell/TableCell";
-import TableBody from "@material-ui/core/TableBody/TableBody";
-
-type CssRules = 'root' | 'table';
-
-const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
- root: {
- width: '100%',
- marginTop: theme.spacing.unit * 3,
- overflowX: 'auto',
- },
- table: {
- minWidth: 700,
- },
-});
-
-interface ProjectListProps {
-}
-
-class ProjectList extends React.Component<ProjectListProps & WithStyles<CssRules>, {}> {
- render() {
- const {classes} = this.props;
- return <Paper className={classes.root}>
- <Table className={classes.table}>
- <TableHead>
- <TableRow>
- <TableCell>Name</TableCell>
- <TableCell>Status</TableCell>
- <TableCell>Type</TableCell>
- <TableCell>Shared by</TableCell>
- <TableCell>File size</TableCell>
- <TableCell>Last modified</TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- <TableRow>
- <TableCell>Project 1</TableCell>
- <TableCell>Complete</TableCell>
- <TableCell>Project</TableCell>
- <TableCell>John Doe</TableCell>
- <TableCell>1.5 GB</TableCell>
- <TableCell>9:22 PM</TableCell>
- </TableRow>
- </TableBody>
- </Table>
- </Paper>;
- }
-}
-
-export default withStyles(styles)(ProjectList);
import { Collapse } from '@material-ui/core';
import CircularProgress from '@material-ui/core/CircularProgress';
-import ProjectTree from './project-tree';
+import { ProjectTree } from './project-tree';
import { TreeItem } from '../../components/tree/tree';
import { ProjectResource } from '../../models/project';
import { mockProjectResource } from '../../models/test-utils';
]
}
];
- const wrapper = mount(<ProjectTree
- projects={project}
- toggleOpen={jest.fn()}
+ const wrapper = mount(<ProjectTree
+ projects={project}
+ toggleOpen={jest.fn()}
toggleActive={jest.fn()}
onContextMenu={jest.fn()} />);
active: true,
status: 1
};
- const wrapper = mount(<ProjectTree
- projects={[project]}
- toggleOpen={jest.fn()}
+ const wrapper = mount(<ProjectTree
+ projects={[project]}
+ toggleOpen={jest.fn()}
toggleActive={jest.fn()}
onContextMenu={jest.fn()} />);
import * as React from 'react';
import { ReactElement } from 'react';
import { StyleRulesCallback, Theme, WithStyles, withStyles } from '@material-ui/core/styles';
-import ListItemText from "@material-ui/core/ListItemText/ListItemText";
-import ListItemIcon from '@material-ui/core/ListItemIcon';
-import Typography from '@material-ui/core/Typography';
-
-import Tree, { TreeItem, TreeItemStatus } from '../../components/tree/tree';
+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';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ marginLeft: `${theme.spacing.unit * 1.5}px`,
+ }
+});
export interface ProjectTreeProps {
projects: Array<TreeItem<ProjectResource>>;
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TreeItem<ProjectResource>) => void;
}
-class ProjectTree<T> extends React.Component<ProjectTreeProps & WithStyles<CssRules>> {
- render(): ReactElement<any> {
- const { classes, projects, toggleOpen, toggleActive, onContextMenu } = this.props;
- const { active, listItemText, row, treeContainer } = classes;
- return (
- <div className={treeContainer}>
- <Tree items={projects}
- onContextMenu={onContextMenu}
- toggleItemOpen={toggleOpen}
- toggleItemActive={toggleActive}
- render={(project: TreeItem<ProjectResource>) =>
- <span className={row}>
- <ListItemIcon className={project.active ? active : ''}>
- <i className="fas fa-folder" />
- </ListItemIcon>
- <ListItemText className={listItemText} primary={
- <Typography className={project.active ? active : ''}>{project.data.name}</Typography>
- } />
- </span>
- } />
- </div>
- );
+export const ProjectTree = withStyles(styles)(
+ class ProjectTreeGeneric<T> extends React.Component<ProjectTreeProps & WithStyles<CssRules>> {
+ render(): ReactElement<any> {
+ const { classes, projects, toggleOpen, toggleActive, onContextMenu } = this.props;
+ return (
+ <div className={classes.root}>
+ <Tree items={projects}
+ onContextMenu={onContextMenu}
+ toggleItemOpen={toggleOpen}
+ toggleItemActive={toggleActive}
+ render={
+ (project: TreeItem<ProjectResource>) =>
+ <ListItemTextIcon
+ icon={ProjectIcon}
+ name={project.data.name}
+ isActive={project.active}
+ hasMargin={true}/>
+ }/>
+ </div>
+ );
+ }
}
-}
-
-type CssRules = 'active' | 'listItemText' | 'row' | 'treeContainer';
-
-const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
- active: {
- color: '#4285F6',
- },
- listItemText: {
- padding: '0px',
- },
- row: {
- display: 'flex',
- alignItems: 'center',
- marginLeft: '20px',
- },
- treeContainer: {
- minWidth: '240px',
- whiteSpace: 'nowrap',
- marginLeft: '13px',
- }
-});
-
-export default withStyles(styles)(ProjectTree);
+);
import { ProjectPanelItem } from './project-panel-item';
import { Grid, Typography, Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
import { formatDate, formatFileSize } from '../../common/formatters';
-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 { RouteComponentProps } from 'react-router';
import { SortDirection } from '../../components/data-table/data-column';
import { ResourceKind } from '../../models/resource';
import { resourceLabel } from '../../common/labels';
-
-export const PROJECT_PANEL_ID = "projectPanel";
-
-export interface ProjectPanelFilter extends DataTableFilterItem {
- type: ResourceKind | ContainerRequestState;
-}
-
-type ProjectPanelProps = {
- currentItemId: string,
- onItemClick: (item: ProjectPanelItem) => void,
- onContextMenu: (event: React.MouseEvent<HTMLElement>, item: ProjectPanelItem) => void;
- onDialogOpen: (ownerUuid: string) => void;
- onItemDoubleClick: (item: ProjectPanelItem) => void,
- onItemRouteChange: (itemId: string) => void
-}
- & DispatchProp
- & WithStyles<CssRules>
- & RouteComponentProps<{ id: string }>;
-
-class ProjectPanel extends React.Component<ProjectPanelProps> {
- render() {
- return <div>
- <div className={this.props.classes.toolbar}>
- <Button color="primary" variant="raised" className={this.props.classes.button}>
- Create a collection
- </Button>
- <Button color="primary" variant="raised" className={this.props.classes.button}>
- Run a process
- </Button>
- <Button color="primary" onClick={() => this.props.onDialogOpen(this.props.currentItemId)} variant="raised" className={this.props.classes.button}>
- New project
- </Button>
- </div>
- <DataExplorer
- id={PROJECT_PANEL_ID}
- onRowClick={this.props.onItemClick}
- onRowDoubleClick={this.props.onItemDoubleClick}
- onContextMenu={this.props.onContextMenu}
- extractKey={(item: ProjectPanelItem) => item.uuid} />
- </div>;
- }
-
- componentWillReceiveProps({ match, currentItemId }: ProjectPanelProps) {
- if (match.params.id !== currentItemId) {
- this.props.onItemRouteChange(match.params.id);
- }
- }
-}
+import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon } from '../../components/icon/icon';
+import { ArvadosTheme } from '../../common/custom-theme';
type CssRules = "toolbar" | "button";
-const styles: StyleRulesCallback<CssRules> = theme => ({
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
toolbar: {
paddingBottom: theme.spacing.unit * 3,
textAlign: "right"
});
const renderName = (item: ProjectPanelItem) =>
- <Grid
- container
- alignItems="center"
- wrap="nowrap"
- spacing={16}>
+ <Grid container alignItems="center" wrap="nowrap" spacing={16}>
<Grid item>
{renderIcon(item)}
</Grid>
const renderIcon = (item: ProjectPanelItem) => {
switch (item.kind) {
case ResourceKind.Project:
- return <i className="fas fa-folder fa-lg" />;
+ return <ProjectIcon />;
case ResourceKind.Collection:
- return <i className="fas fa-archive fa-lg" />;
+ return <CollectionIcon />;
case ResourceKind.Process:
- return <i className="fas fa-cogs fa-lg" />;
+ return <ProcessIcon />;
default:
- return <i />;
+ return <DefaultIcon />;
}
};
-const renderDate = (date: string) =>
- <Typography noWrap>
- {formatDate(date)}
- </Typography>;
+const renderDate = (date: string) => {
+ return <Typography noWrap>{formatDate(date)}</Typography>;
+};
const renderFileSize = (fileSize?: number) =>
<Typography noWrap>
</Typography>;
const renderOwner = (owner: string) =>
- <Typography noWrap color="primary">
+ <Typography noWrap color="primary" >
{owner}
</Typography>;
-const renderType = (type: string) => {
- return <Typography noWrap>
+const renderType = (type: string) =>
+ <Typography noWrap>
{resourceLabel(type)}
</Typography>;
-};
const renderStatus = (item: ProjectPanelItem) =>
- <Typography noWrap align="center">
+ <Typography noWrap align="center" >
{item.status || "-"}
</Typography>;
export enum ProjectPanelColumnNames {
- Name = "Name",
- Status = "Status",
- Type = "Type",
- Owner = "Owner",
- FileSize = "File size",
- LastModified = "Last modified"
+ NAME = "Name",
+ STATUS = "Status",
+ TYPE = "Type",
+ OWNER = "Owner",
+ FILE_SIZE = "File size",
+ LAST_MODIFIED = "Last modified"
+}
+export interface ProjectPanelFilter extends DataTableFilterItem {
+ type: ResourceKind | ContainerRequestState;
}
-export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [{
- name: ProjectPanelColumnNames.Name,
- selected: true,
- sortDirection: SortDirection.Asc,
- render: renderName,
- width: "450px"
-}, {
- name: "Status",
- selected: true,
- filters: [{
- name: ContainerRequestState.Committed,
+export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
+ {
+ name: ProjectPanelColumnNames.NAME,
selected: true,
- type: ContainerRequestState.Committed
- }, {
- name: ContainerRequestState.Final,
+ sortDirection: SortDirection.Asc,
+ render: renderName,
+ width: "450px"
+ },
+ {
+ name: "Status",
selected: true,
- type: ContainerRequestState.Final
- }, {
- name: ContainerRequestState.Uncommitted,
+ filters: [
+ {
+ name: ContainerRequestState.Committed,
+ selected: true,
+ type: ContainerRequestState.Committed
+ },
+ {
+ name: ContainerRequestState.Final,
+ selected: true,
+ type: ContainerRequestState.Final
+ },
+ {
+ name: ContainerRequestState.Uncommitted,
+ selected: true,
+ type: ContainerRequestState.Uncommitted
+ }
+ ],
+ render: renderStatus,
+ width: "75px"
+ },
+ {
+ name: ProjectPanelColumnNames.TYPE,
selected: true,
- type: ContainerRequestState.Uncommitted
- }],
- render: renderStatus,
- width: "75px"
-}, {
- name: ProjectPanelColumnNames.Type,
- selected: true,
- filters: [{
- name: resourceLabel(ResourceKind.Collection),
+ filters: [
+ {
+ name: resourceLabel(ResourceKind.Collection),
+ selected: true,
+ type: ResourceKind.Collection
+ },
+ {
+ name: resourceLabel(ResourceKind.Process),
+ selected: true,
+ type: ResourceKind.Process
+ },
+ {
+ name: resourceLabel(ResourceKind.Project),
+ selected: true,
+ type: ResourceKind.Project
+ }
+ ],
+ render: item => renderType(item.kind),
+ width: "125px"
+ },
+ {
+ name: ProjectPanelColumnNames.OWNER,
selected: true,
- type: ResourceKind.Collection
- }, {
- name: resourceLabel(ResourceKind.Process),
+ render: item => renderOwner(item.owner),
+ width: "200px"
+ },
+ {
+ name: ProjectPanelColumnNames.FILE_SIZE,
selected: true,
- type: ResourceKind.Process
- }, {
- name: resourceLabel(ResourceKind.Project),
+ render: item => renderFileSize(item.fileSize),
+ width: "50px"
+ },
+ {
+ name: ProjectPanelColumnNames.LAST_MODIFIED,
selected: true,
- type: ResourceKind.Project
- }],
- render: item => renderType(item.kind),
- width: "125px"
-}, {
- name: ProjectPanelColumnNames.Owner,
- selected: true,
- render: item => renderOwner(item.owner),
- width: "200px"
-}, {
- name: ProjectPanelColumnNames.FileSize,
- selected: true,
- render: item => renderFileSize(item.fileSize),
- width: "50px"
-}, {
- name: ProjectPanelColumnNames.LastModified,
- selected: true,
- sortDirection: SortDirection.None,
- render: item => renderDate(item.lastModified),
- width: "150px"
-}];
-
-
-export default withStyles(styles)(
+ sortDirection: SortDirection.None,
+ render: item => renderDate(item.lastModified),
+ width: "150px"
+ }
+];
+
+export const PROJECT_PANEL_ID = "projectPanel";
+
+interface ProjectPanelDataProps {
+ currentItemId: string;
+}
+
+interface ProjectPanelActionProps {
+ onItemClick: (item: ProjectPanelItem) => void;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, item: ProjectPanelItem) => void;
+ onDialogOpen: (ownerUuid: string) => void;
+ onItemDoubleClick: (item: ProjectPanelItem) => void;
+ onItemRouteChange: (itemId: string) => void;
+}
+
+type ProjectPanelProps = ProjectPanelDataProps & ProjectPanelActionProps & DispatchProp
+ & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+
+export const ProjectPanel = withStyles(styles)(
connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))(
- ProjectPanel));
+ class extends React.Component<ProjectPanelProps> {
+ render() {
+ const { classes } = this.props;
+ return <div>
+ <div className={classes.toolbar}>
+ <Button color="primary" variant="raised" className={classes.button}>
+ Create a collection
+ </Button>
+ <Button color="primary" variant="raised" className={classes.button}>
+ Run a process
+ </Button>
+ <Button color="primary" onClick={this.handleNewProjectClick} variant="raised" className={classes.button}>
+ New project
+ </Button>
+ </div>
+ <DataExplorer
+ id={PROJECT_PANEL_ID}
+ onRowClick={this.props.onItemClick}
+ onRowDoubleClick={this.props.onItemDoubleClick}
+ onContextMenu={this.props.onContextMenu}
+ extractKey={(item: ProjectPanelItem) => item.uuid} />
+ </div>;
+ }
+
+ handleNewProjectClick = () => {
+ this.props.onDialogOpen(this.props.currentItemId);
+ }
+ componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: ProjectPanelProps) {
+ if (match.params.id !== currentItemId) {
+ onItemRouteChange(match.params.id);
+ }
+ }
+ }
+ )
+);
\ No newline at end of file
import * as React from 'react';
import * as ReactDOM from 'react-dom';
-import Workbench from '../../views/workbench/workbench';
+import { Workbench } from '../../views/workbench/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 Drawer from '@material-ui/core/Drawer';
import { connect, DispatchProp } from "react-redux";
import { Route, Switch, RouteComponentProps } from "react-router";
-import authActions from "../../store/auth/auth-action";
+import { authActions } from "../../store/auth/auth-action";
import { User } from "../../models/user";
import { RootState } from "../../store/store";
-import MainAppBar, {
- MainAppBarActionProps,
- MainAppBarMenuItem
-} from '../../views-components/main-app-bar/main-app-bar';
+import { MainAppBar, MainAppBarActionProps, MainAppBarMenuItem } from '../../views-components/main-app-bar/main-app-bar';
import { Breadcrumb } from '../../components/breadcrumbs/breadcrumbs';
import { push } from 'react-router-redux';
-import ProjectTree from '../../views-components/project-tree/project-tree';
+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 { 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 ProjectPanel from "../project-panel/project-panel";
-import DetailsPanel from '../../views-components/details-panel/details-panel';
+import { projectActions } from "../../store/project/project-action";
+import { ProjectPanel } from "../project-panel/project-panel";
+import { DetailsPanel } from '../../views-components/details-panel/details-panel';
import { ArvadosTheme } from '../../common/custom-theme';
-import ContextMenu, { ContextMenuAction } from '../../components/context-menu/context-menu';
-import { mockAnchorFromMouseEvent } from '../../components/popover/helpers';
-import DialogProjectCreate from "../../views-components/create-project-dialog/create-project-dialog";
+import { CreateProjectDialog } from "../../views-components/create-project-dialog/create-project-dialog";
import { authService } from '../../services/services';
-import detailsPanelActions, { loadDetails } from "../../store/details-panel/details-panel-action";
+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";
const drawerWidth = 240;
const appBarHeight = 100;
}
interface WorkbenchState {
- contextMenu: {
- anchorEl?: HTMLElement;
- itemUuid?: string;
- };
anchorEl: any;
searchText: string;
menuItems: {
};
}
-
-class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
- state = {
- contextMenu: {
- anchorEl: undefined,
- itemUuid: undefined
- },
- isCreationDialogOpen: false,
- anchorEl: null,
- searchText: "",
- breadcrumbs: [],
- menuItems: {
- accountMenu: [
- {
- label: "Logout",
- action: () => this.props.dispatch(authActions.LOGOUT())
- },
- {
- label: "My account",
- action: () => this.props.dispatch(push("/my-account"))
- }
- ],
- helpMenu: [
- {
- label: "Help",
- action: () => this.props.dispatch(push("/help"))
+export const Workbench = withStyles(styles)(
+ connect<WorkbenchDataProps>(
+ (state: RootState) => ({
+ projects: state.projects.items,
+ currentProjectId: state.projects.currentItemId,
+ user: state.auth.user,
+ sidePanelItems: state.sidePanel
+ })
+ )(
+ class extends React.Component<WorkbenchProps, WorkbenchState> {
+ state = {
+ isCreationDialogOpen: false,
+ anchorEl: null,
+ searchText: "",
+ breadcrumbs: [],
+ menuItems: {
+ accountMenu: [
+ {
+ label: "Logout",
+ action: () => this.props.dispatch(authActions.LOGOUT())
+ },
+ {
+ label: "My account",
+ action: () => this.props.dispatch(push("/my-account"))
+ }
+ ],
+ helpMenu: [
+ {
+ label: "Help",
+ action: () => this.props.dispatch(push("/help"))
+ }
+ ],
+ anonymousMenu: [
+ {
+ label: "Sign in",
+ action: () => this.props.dispatch(authActions.LOGIN())
+ }
+ ]
}
- ],
- anonymousMenu: [
- {
- label: "Sign in",
- action: () => this.props.dispatch(authActions.LOGIN())
- }
- ]
- }
- };
+ };
- mainAppBarActions: MainAppBarActionProps = {
- onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
- this.props.dispatch<any>(setProjectItem(itemId, ItemMode.BOTH));
- this.props.dispatch<any>(loadDetails(itemId, ResourceKind.Project));
- },
- onSearch: searchText => {
- this.setState({ searchText });
- this.props.dispatch(push(`/search?q=${searchText}`));
- },
- onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action(),
- onDetailsPanelToggle: () => {
- this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
- },
- onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: NavBreadcrumb) => {
- this.openContextMenu(event, breadcrumb.itemId);
- }
- };
+ render() {
+ const path = getTreePath(this.props.projects, this.props.currentProjectId);
+ const breadcrumbs = path.map(item => ({
+ label: item.data.name,
+ itemId: item.data.uuid,
+ status: item.status
+ }));
- toggleSidePanelOpen = (itemId: string) => {
- this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId));
- }
+ const { classes, user } = this.props;
+ return (
+ <div className={classes.root}>
+ <div className={classes.appBar}>
+ <MainAppBar
+ breadcrumbs={breadcrumbs}
+ searchText={this.state.searchText}
+ user={this.props.user}
+ menuItems={this.state.menuItems}
+ {...this.mainAppBarActions} />
+ </div>
+ {user &&
+ <Drawer
+ variant="permanent"
+ classes={{
+ paper: classes.drawerPaper,
+ }}>
+ <div className={classes.toolbar} />
+ <SidePanel
+ toggleOpen={this.toggleSidePanelOpen}
+ toggleActive={this.toggleSidePanelActive}
+ sidePanelItems={this.props.sidePanelItems}
+ onContextMenu={(event) => this.openContextMenu(event, authService.getUuid() || "", ContextMenuKind.RootProject)}>
+ <ProjectTree
+ projects={this.props.projects}
+ toggleOpen={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.OPEN))}
+ onContextMenu={(event, item) => this.openContextMenu(event, item.data.uuid, ContextMenuKind.Project)}
+ toggleActive={itemId => {
+ this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE));
+ this.props.dispatch<any>(loadDetails(itemId, ResourceKind.Project));
+ this.props.dispatch<any>(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.Projects));
+ }} />
+ </SidePanel>
+ </Drawer>}
+ <main className={classes.contentWrapper}>
+ <div className={classes.content}>
+ <Switch>
+ <Route path="/projects/:id" render={this.renderProjectPanel} />
+ </Switch>
+ </div>
+ {user && <DetailsPanel />}
+ </main>
+ <ContextMenu />
+ <CreateProjectDialog />
+ </div>
+ );
+ }
- toggleSidePanelActive = (itemId: string) => {
- this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(itemId));
- this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
- this.props.dispatch(push("/"));
- }
+ renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => <ProjectPanel
+ onItemRouteChange={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE))}
+ onContextMenu={(event, item) => this.openContextMenu(event, item.uuid, ContextMenuKind.Project)}
+ onDialogOpen={this.handleCreationDialogOpen}
+ onItemClick={item => {
+ this.props.dispatch<any>(loadDetails(item.uuid, item.kind as ResourceKind));
+ }}
+ onItemDoubleClick={item => {
+ this.props.dispatch<any>(setProjectItem(item.uuid, ItemMode.ACTIVE));
+ this.props.dispatch<any>(loadDetails(item.uuid, ResourceKind.Project));
+ }}
+ {...props} />
- handleCreationDialogOpen = (itemUuid: string) => {
- this.closeContextMenu();
- this.props.dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: itemUuid }));
- }
+ mainAppBarActions: MainAppBarActionProps = {
+ onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
+ this.props.dispatch<any>(setProjectItem(itemId, ItemMode.BOTH));
+ this.props.dispatch<any>(loadDetails(itemId, ResourceKind.Project));
+ },
+ onSearch: searchText => {
+ this.setState({ searchText });
+ this.props.dispatch(push(`/search?q=${searchText}`));
+ },
+ onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action(),
+ onDetailsPanelToggle: () => {
+ this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
+ },
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: NavBreadcrumb) => {
+ this.openContextMenu(event, breadcrumb.itemId, ContextMenuKind.Project);
+ }
+ };
+ toggleSidePanelOpen = (itemId: string) => {
+ this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId));
+ }
- openContextMenu = (event: React.MouseEvent<HTMLElement>, itemUuid: string) => {
- event.preventDefault();
- this.setState({
- contextMenu: {
- anchorEl: mockAnchorFromMouseEvent(event),
- itemUuid
+ toggleSidePanelActive = (itemId: string) => {
+ this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(itemId));
+ this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
+ this.props.dispatch(push("/"));
}
- });
- }
- closeContextMenu = () => {
- this.setState({ contextMenu: {} });
- }
+ handleCreationDialogOpen = (itemUuid: string) => {
+ this.props.dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: itemUuid }));
+ }
- openCreateDialog = (item: ContextMenuAction) => {
- const { itemUuid } = this.state.contextMenu;
- if (item.openCreateDialog && itemUuid) {
- this.handleCreationDialogOpen(itemUuid);
+ openContextMenu = (event: React.MouseEvent<HTMLElement>, itemUuid: string, kind: ContextMenuKind) => {
+ event.preventDefault();
+ this.props.dispatch(
+ contextMenuActions.OPEN_CONTEXT_MENU({
+ position: { x: event.clientX, y: event.clientY },
+ resource: { uuid: itemUuid, kind }
+ })
+ );
+ }
}
- }
-
- render() {
- const path = getTreePath(this.props.projects, this.props.currentProjectId);
- const breadcrumbs = path.map(item => ({
- label: item.data.name,
- itemId: item.data.uuid,
- status: item.status
- }));
-
- const { classes, user } = this.props;
- return (
- <div className={classes.root}>
- <div className={classes.appBar}>
- <MainAppBar
- breadcrumbs={breadcrumbs}
- searchText={this.state.searchText}
- user={this.props.user}
- menuItems={this.state.menuItems}
- {...this.mainAppBarActions} />
- </div>
- {user &&
- <Drawer
- variant="permanent"
- classes={{
- paper: classes.drawerPaper,
- }}>
- <div className={classes.toolbar} />
- <SidePanel
- toggleOpen={this.toggleSidePanelOpen}
- toggleActive={this.toggleSidePanelActive}
- sidePanelItems={this.props.sidePanelItems}
- onContextMenu={(event) => this.openContextMenu(event, authService.getUuid() || "")}>
- <ProjectTree
- projects={this.props.projects}
- toggleOpen={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.OPEN))}
- onContextMenu={(event, item) => this.openContextMenu(event, item.data.uuid)}
- toggleActive={itemId => {
- this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE));
- this.props.dispatch<any>(loadDetails(itemId, ResourceKind.Project));
- this.props.dispatch<any>(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.Projects));
- }} />
- </SidePanel>
- </Drawer>}
- <main className={classes.contentWrapper}>
- <div className={classes.content}>
- <Switch>
- <Route path="/projects/:id" render={this.renderProjectPanel} />
- </Switch>
- </div>
- <DetailsPanel />
- </main>
- <ContextMenu
- anchorEl={this.state.contextMenu.anchorEl}
- actions={contextMenuActions}
- onActionClick={this.openCreateDialog}
- onClose={this.closeContextMenu} />
- <DialogProjectCreate />
- </div>
- );
- }
-
- renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => <ProjectPanel
- onItemRouteChange={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE))}
- onContextMenu={(event, item) => this.openContextMenu(event, item.uuid)}
- onDialogOpen={this.handleCreationDialogOpen}
- onItemClick={item => {
- this.props.dispatch<any>(loadDetails(item.uuid, item.kind as ResourceKind));
- }}
- onItemDoubleClick={item => {
- this.props.dispatch<any>(setProjectItem(item.uuid, ItemMode.ACTIVE));
- this.props.dispatch<any>(loadDetails(item.uuid, ResourceKind.Project));
- }}
- {...props} />
-}
-
-const contextMenuActions = [[{
- icon: "fas fa-plus fa-fw",
- name: "New project",
- openCreateDialog: true
-}, {
- icon: "fas fa-users fa-fw",
- name: "Share"
-}, {
- icon: "fas fa-sign-out-alt fa-fw",
- name: "Move to"
-}, {
- icon: "fas fa-star fa-fw",
- name: "Add to favourite"
-}, {
- icon: "fas fa-edit fa-fw",
- name: "Rename"
-}, {
- icon: "fas fa-copy fa-fw",
- name: "Make a copy"
-}, {
- icon: "fas fa-download fa-fw",
- name: "Download"
-}], [{
- icon: "fas fa-trash-alt fa-fw",
- name: "Remove"
-}
-]];
-
-export default connect<WorkbenchDataProps>(
- (state: RootState) => ({
- projects: state.projects.items,
- currentProjectId: state.projects.currentItemId,
- user: state.auth.user,
- sidePanelItems: state.sidePanel
- })
-)(
- withStyles(styles)(Workbench)
+ )
);
"linterOptions": {
"exclude": [
"config/**/*.js",
- "node_modules/**/*.ts"
+ "node_modules/**/*.ts",
+ "coverage/lcov-report/*.js"
]
}
}
"@babel/code-frame@^7.0.0-beta.35":
- version "7.0.0-beta.51"
- resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.51.tgz#bd71d9b192af978df915829d39d4094456439a0c"
+ version "7.0.0-beta.54"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.54.tgz#0024f96fdf7028a21d68e273afd4e953214a1ead"
dependencies:
- "@babel/highlight" "7.0.0-beta.51"
+ "@babel/highlight" "7.0.0-beta.54"
-"@babel/highlight@7.0.0-beta.51":
- version "7.0.0-beta.51"
- resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.51.tgz#e8844ae25a1595ccfd42b89623b4376ca06d225d"
+"@babel/highlight@7.0.0-beta.54":
+ version "7.0.0-beta.54"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.54.tgz#155d507358329b8e7068970017c3fd74a9b08584"
dependencies:
chalk "^2.0.0"
esutils "^2.0.2"
js-tokens "^3.0.0"
"@babel/runtime@^7.0.0-beta.42":
- version "7.0.0-beta.51"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.51.tgz#48b8ed18307034c6620f643514650ca2ccc0165a"
+ version "7.0.0-beta.54"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.54.tgz#39ebb42723fe7ca4b3e1b00e967e80138d47cadf"
dependencies:
core-js "^2.5.7"
- regenerator-runtime "^0.11.1"
+ regenerator-runtime "^0.12.0"
-"@material-ui/core@1.2.1":
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-1.2.1.tgz#f8c73da10b875762b37be7167ec2ac79b027499f"
+"@material-ui/core@1.4.0":
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-1.4.0.tgz#e535fef84576b096c46e1fb7d6c4c61895155fd3"
dependencies:
"@babel/runtime" "^7.0.0-beta.42"
"@types/jss" "^9.5.3"
deepmerge "^2.0.1"
dom-helpers "^3.2.1"
hoist-non-react-statics "^2.5.0"
+ is-plain-object "^2.0.4"
jss "^9.3.3"
jss-camel-case "^6.0.0"
jss-default-unit "^8.0.2"
jss-vendor-prefixer "^7.0.0"
keycode "^2.1.9"
normalize-scroll-left "^0.1.2"
+ popper.js "^1.0.0"
prop-types "^15.6.0"
react-event-listener "^0.6.0"
react-jss "^8.1.0"
- react-popper "^0.10.0"
react-transition-group "^2.2.1"
- recompose "^0.26.0 || ^0.27.0"
+ recompose "^0.27.0"
scroll "^2.0.3"
warning "^4.0.1"
recompose "^0.26.0 || ^0.27.0"
"@types/cheerio@*":
- version "0.22.7"
- resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.7.tgz#4a92eafedfb2b9f4437d3a4410006d81114c66ce"
+ version "0.22.8"
+ resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.8.tgz#5702f74f78b73e13f1eb1bd435c2c9de61a250d4"
"@types/classnames@^2.2.4":
- version "2.2.4"
- resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.4.tgz#d3ee9ebf714aa34006707b8f4a58fd46b642305a"
+ version "2.2.5"
+ resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.5.tgz#62945b24b48dc02fb32e89252bde3daf942c4235"
"@types/enzyme-adapter-react-16@1.0.2":
version "1.0.2"
dependencies:
"@types/enzyme" "*"
-"@types/enzyme@*", "@types/enzyme@3.1.10":
- version "3.1.10"
- resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.10.tgz#28108a9864e65699751469551a803a35d2e26160"
+"@types/enzyme@*", "@types/enzyme@3.1.12":
+ version "3.1.12"
+ resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.12.tgz#293bb07c1ef5932d37add3879e72e0f5bc614f3c"
dependencies:
"@types/cheerio" "*"
"@types/react" "*"
version "4.6.2"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0"
-"@types/jest@23.1.0":
- version "23.1.0"
- resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.1.0.tgz#8054dd838ba23dc331794d26456b86c7e50bf0f6"
+"@types/jest@23.3.0":
+ version "23.3.0"
+ resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.0.tgz#5dd70033b616a6228042244ebd992f6426808810"
"@types/jss@^9.5.3":
version "9.5.3"
csstype "^2.0.0"
indefinite-observable "^1.0.1"
-"@types/lodash@4.14.109":
- version "4.14.109"
- resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.109.tgz#b1c4442239730bf35cabaf493c772b18c045886d"
+"@types/lodash@4.14.112":
+ version "4.14.112"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.112.tgz#4a8d8e5716b97a1ac01fe1931ad1e4cba719de5a"
-"@types/node@*", "@types/node@10.3.3":
- version "10.3.3"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-10.3.3.tgz#8798d9e39af2fa604f715ee6a6b19796528e46c3"
+"@types/node@*", "@types/node@10.5.2":
+ version "10.5.2"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.2.tgz#f19f05314d5421fe37e74153254201a7bf00a707"
"@types/react-dom@16.0.6":
version "16.0.6"
"@types/node" "*"
"@types/react" "*"
-"@types/react-redux@6.0.2":
- version "6.0.2"
- resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-6.0.2.tgz#10069b53db8e0920fd8656e068dcf10c53c9ad2a"
+"@types/react-redux@6.0.4":
+ version "6.0.4"
+ resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-6.0.4.tgz#c1cfce0a0bd88983c75dbf393576f8dc59181586"
dependencies:
"@types/react" "*"
redux "^4.0.0"
"@types/react-router" "*"
redux ">= 3.7.2"
-"@types/react-router@*", "@types/react-router@4.0.26":
- version "4.0.26"
- resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-4.0.26.tgz#4489c873642baa633014294a6d0a290001ba9860"
+"@types/react-router@*", "@types/react-router@4.0.29":
+ version "4.0.29"
+ resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-4.0.29.tgz#1a906dd99abf21297a5b7cf003d1fd36e7a92069"
dependencies:
"@types/history" "*"
"@types/react" "*"
dependencies:
"@types/react" "*"
-"@types/react@*", "@types/react@16.3":
- version "16.3.18"
- resolved "https://registry.yarnpkg.com/@types/react/-/react-16.3.18.tgz#bf195aed4d77dc86f06e4c9bb760214a3b822b8d"
+"@types/react@*", "@types/react@16.4":
+ version "16.4.6"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-16.4.6.tgz#5024957c6bcef4f02823accf5974faba2e54fada"
dependencies:
csstype "^2.2.0"
json-schema-traverse "^0.3.0"
ajv@^6.1.0:
- version "6.5.1"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.1.tgz#88ebc1263c7133937d108b80c5572e64e1d9322d"
+ version "6.5.2"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.2.tgz#678495f9b82f7cca6be248dd92f59bff5e1f4360"
dependencies:
fast-deep-equal "^2.0.1"
fast-json-stable-stringify "^2.0.0"
babel-preset-jest "^22.4.4"
babel-loader@^7.1.2:
- version "7.1.4"
- resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.4.tgz#e3463938bd4e6d55d1c174c5485d406a188ed015"
+ version "7.1.5"
+ resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.5.tgz#e3ee0cd7394aa557e013b02d3e492bfd07aa6d68"
dependencies:
find-cache-dir "^1.0.0"
loader-utils "^1.0.2"
babel-plugin-syntax-object-rest-spread "^6.13.0"
babel-preset-react-app@^3.1.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/babel-preset-react-app/-/babel-preset-react-app-3.1.1.tgz#d3f06a79742f0e89d7afcb72e282d9809c850920"
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/babel-preset-react-app/-/babel-preset-react-app-3.1.2.tgz#49ba3681b917c4e5c73a5249d3ef4c48fae064e2"
dependencies:
babel-plugin-dynamic-import-node "1.1.0"
babel-plugin-syntax-dynamic-import "6.18.0"
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
bcrypt-pbkdf@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
dependencies:
tweetnacl "^0.14.3"
resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz#425d68a58d3447f02a04aa894187fce8af8b7b8e"
browser-resolve@^1.11.2:
- version "1.11.2"
- resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.2.tgz#8ff09b0a2c421718a1051c260b32e48f442938ce"
+ version "1.11.3"
+ resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6"
dependencies:
resolve "1.1.7"
evp_bytestokey "^1.0.0"
browserify-des@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.1.tgz#3343124db6d7ad53e26a8826318712bdc8450f9c"
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c"
dependencies:
cipher-base "^1.0.1"
des.js "^1.0.0"
inherits "^2.0.1"
+ safe-buffer "^5.1.2"
browserify-rsa@^4.0.0:
version "4.0.1"
lodash.uniq "^4.5.0"
caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
- version "1.0.30000856"
- resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000856.tgz#fbebb99abe15a5654fc7747ebb5315bdfde3358f"
+ version "1.0.30000867"
+ resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000867.tgz#b55a6ecfac3107988940c9c7dfe1866315312c97"
caniuse-lite@^1.0.30000748, caniuse-lite@^1.0.30000792:
- version "1.0.30000856"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000856.tgz#ecc16978135a6f219b138991eb62009d25ee8daa"
+ version "1.0.30000865"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000865.tgz#70026616e8afe6e1442f8bb4e1092987d81a2f25"
capture-exit@^1.2.0:
version "1.2.0"
fsevents "^1.0.0"
chokidar@^2.0.2:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.3.tgz#dcbd4f6cbb2a55b4799ba8a840ac527e5f4b1176"
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26"
dependencies:
anymatch "^2.0.0"
async-each "^1.0.0"
inherits "^2.0.1"
is-binary-path "^1.0.0"
is-glob "^4.0.0"
+ lodash.debounce "^4.0.8"
normalize-path "^2.1.1"
path-is-absolute "^1.0.0"
readdirp "^2.0.0"
- upath "^1.0.0"
+ upath "^1.0.5"
optionalDependencies:
- fsevents "^1.1.2"
+ fsevents "^1.2.2"
chownr@^1.0.1:
version "1.0.1"
dependencies:
delayed-stream "~1.0.0"
-commander@2.15.x, commander@^2.12.1, commander@~2.15.0:
- version "2.15.1"
- resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
+commander@2.16.x, commander@^2.12.1, commander@~2.16.0:
+ version "2.16.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50"
commander@~2.13.0:
version "2.13.0"
version "1.2.1"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
-compressible@~2.0.13:
+compressible@~2.0.14:
version "2.0.14"
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.14.tgz#326c5f507fbb055f54116782b969a81b67a29da7"
dependencies:
mime-db ">= 1.34.0 < 2"
compression@^1.5.2:
- version "1.7.2"
- resolved "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz#aaffbcd6aaf854b44ebb280353d5ad1651f59a69"
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.3.tgz#27e0e176aaf260f7f2c2813c3e440adb9f1993db"
dependencies:
- accepts "~1.3.4"
+ accepts "~1.3.5"
bytes "3.0.0"
- compressible "~2.0.13"
+ compressible "~2.0.14"
debug "2.6.9"
on-headers "~1.0.1"
- safe-buffer "5.1.1"
+ safe-buffer "5.1.2"
vary "~1.1.2"
concat-map@0.0.1:
source-map "^0.5.3"
cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
- version "0.3.2"
- resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.2.tgz#b8036170c79f07a90ff2f16e22284027a243848b"
+ version "0.3.4"
+ resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.4.tgz#8cd52e8a3acfd68d3aed38ee0a640177d2f9d797"
"cssstyle@>= 0.3.1 < 0.4.0":
version "0.3.1"
dependencies:
cssom "0.3.x"
-csstype@^2.0.0, csstype@^2.5.2:
- version "2.5.3"
- resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.3.tgz#2504152e6e1cc59b32098b7f5d6a63f16294c1f7"
-
-csstype@^2.2.0:
+csstype@^2.0.0, csstype@^2.2.0, csstype@^2.5.2:
version "2.5.5"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.5.tgz#4125484a3d42189a863943f23b9e4b80fedfa106"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30:
- version "1.3.48"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.48.tgz#d3b0d8593814044e092ece2108fc3ac9aea4b900"
+ version "1.3.52"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.52.tgz#d2d9f1270ba4a3b967b831c40ef71fb4d9ab5ce0"
elliptic@^6.0.0:
version "6.4.0"
react-test-renderer "^16.0.0-0"
enzyme-adapter-utils@^1.3.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.3.0.tgz#d6c85756826c257a8544d362cc7a67e97ea698c7"
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.4.0.tgz#c403b81e8eb9953658569e539780964bdc98de62"
dependencies:
- lodash "^4.17.4"
- object.assign "^4.0.4"
+ object.assign "^4.1.0"
prop-types "^15.6.0"
enzyme@^3.3.0:
prr "~1.0.1"
error-ex@^1.2.0:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
dependencies:
is-arrayish "^0.2.1"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
escodegen@^1.9.0:
- version "1.10.0"
- resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.10.0.tgz#f647395de22519fbd0d928ffcf1d17e0dec2603e"
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589"
dependencies:
esprima "^3.1.3"
estraverse "^4.2.0"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
esprima@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
esrecurse@^4.1.0:
version "4.2.1"
safe-buffer "^5.1.1"
exec-sh@^0.2.0:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.1.tgz#163b98a6e89e6b65b47c2a28d215bc1f63989c38"
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36"
dependencies:
- merge "^1.1.3"
+ merge "^1.2.0"
execa@^0.7.0:
version "0.7.0"
readable-stream "^2.0.4"
follow-redirects@^1.0.0, follow-redirects@^1.3.0:
- version "1.5.0"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.0.tgz#234f49cf770b7f35b40e790f636ceba0c3a0ab77"
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.1.tgz#67a8f14f5a1f67f962c2c46469c79eaec0a90291"
dependencies:
debug "^3.1.0"
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
-fsevents@^1.0.0, fsevents@^1.1.2, fsevents@^1.1.3, fsevents@^1.2.3:
+fsevents@^1.0.0, fsevents@^1.1.3, fsevents@^1.2.2, fsevents@^1.2.3:
version "1.2.4"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426"
dependencies:
wide-align "^1.1.0"
get-caller-file@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
get-stdin@^4.0.1:
version "4.0.1"
safe-buffer "^5.0.1"
hash.js@^1.0.0, hash.js@^1.0.3:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.4.tgz#8b50e1f35d51bd01e5ed9ece4dbe3549ccfa0a3c"
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.5.tgz#e38ab4b85dfb1e0c40fe9265c0e9b54854c23812"
dependencies:
inherits "^2.0.3"
- minimalistic-assert "^1.0.0"
+ minimalistic-assert "^1.0.1"
he@1.1.x:
version "1.1.1"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0:
- version "2.5.4"
- resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.4.tgz#fc3b1ac05d2ae3abedec84eba846511b0d4fcc4f"
+ version "2.5.5"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
hoist-non-react-statics@^2.5.4:
version "2.5.5"
parse-passwd "^1.0.0"
hosted-git-info@^2.1.4:
- version "2.6.0"
- resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222"
+ version "2.7.1"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
hpack.js@^2.1.6:
version "2.1.6"
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
html-minifier@^3.2.3:
- version "3.5.16"
- resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.16.tgz#39f5aabaf78bdfc057fe67334226efd7f3851175"
+ version "3.5.19"
+ resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.19.tgz#ed53c4b7326fe507bc3a1adbcc3bbb56660a2ebd"
dependencies:
camel-case "3.0.x"
clean-css "4.1.x"
- commander "2.15.x"
+ commander "2.16.x"
he "1.1.x"
param-case "2.1.x"
relateurl "0.2.x"
- uglify-js "3.3.x"
+ uglify-js "3.4.x"
html-webpack-plugin@2.29.0:
version "2.29.0"
builtin-modules "^1.0.0"
is-callable@^1.1.1, is-callable@^1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2"
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
is-ci@^1.0.10:
version "1.1.0"
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
-is-odd@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-2.0.0.tgz#7646624671fd7ea558ccd9a2795182f2958f1b24"
- dependencies:
- is-number "^4.0.0"
-
is-path-cwd@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d"
jest-cli "^22.4.2"
js-base64@^2.1.9:
- version "2.4.5"
- resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.5.tgz#e293cd3c7c82f070d700fc7a1ca0a2e69f101f92"
+ version "2.4.8"
+ resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.8.tgz#57a9b130888f956834aa40c5b165ba59c758f033"
js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+"js-tokens@^3.0.0 || ^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+
js-yaml@^3.4.3, js-yaml@^3.7.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1"
css-vendor "^0.3.8"
jss@^9.3.3, jss@^9.7.0:
- version "9.8.3"
- resolved "https://registry.yarnpkg.com/jss/-/jss-9.8.3.tgz#399da571c4b2c8f4cf418ca7e8627e44fc287fc8"
+ version "9.8.7"
+ resolved "https://registry.yarnpkg.com/jss/-/jss-9.8.7.tgz#ed9763fc0f2f0260fc8260dac657af61e622ce05"
dependencies:
is-in-browser "^1.1.3"
symbol-observable "^1.1.0"
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
+lodash.debounce@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
+
lodash.defaults@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
dependencies:
- js-tokens "^3.0.0"
+ js-tokens "^3.0.0 || ^4.0.0"
loud-rejection@^1.0.0:
version "1.6.0"
dependencies:
readable-stream "^2.0.1"
-merge@^1.1.3:
+merge@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da"
bn.js "^4.0.0"
brorand "^1.0.1"
-"mime-db@>= 1.34.0 < 2":
- version "1.34.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.34.0.tgz#452d0ecff5c30346a6dc1e64b1eaee0d3719ff9a"
-
-mime-db@~1.33.0:
- version "1.33.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
+"mime-db@>= 1.34.0 < 2", mime-db@~1.35.0:
+ version "1.35.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.35.0.tgz#0569d657466491283709663ad379a99b90d9ab47"
mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18:
- version "2.1.18"
- resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
+ version "2.1.19"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.19.tgz#71e464537a7ef81c15f2db9d97e913fc0ff606f0"
dependencies:
- mime-db "~1.33.0"
+ mime-db "~1.35.0"
mime@1.4.1:
version "1.4.1"
dependencies:
dom-walk "^0.1.0"
-minimalistic-assert@^1.0.0:
+minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
nanomatch@^1.2.9:
- version "1.2.9"
- resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.9.tgz#879f7150cb2dab7a471259066c104eee6e0fa7c2"
+ version "1.2.13"
+ resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
dependencies:
arr-diff "^4.0.0"
array-unique "^0.3.2"
define-property "^2.0.2"
extend-shallow "^3.0.2"
fragment-cache "^0.2.1"
- is-odd "^2.0.0"
is-windows "^1.0.2"
kind-of "^6.0.2"
object.pick "^1.3.0"
randexp "0.4.6"
semver "^5.4.1"
-needle@^2.2.0:
+needle@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.1.tgz#b5e325bd3aae8c2678902fa296f729455d1d3a7d"
dependencies:
which "^1.3.0"
node-pre-gyp@^0.10.0:
- version "0.10.0"
- resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz#6e4ef5bb5c5203c6552448828c852c40111aac46"
+ version "0.10.3"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc"
dependencies:
detect-libc "^1.0.2"
mkdirp "^0.5.1"
- needle "^2.2.0"
+ needle "^2.2.1"
nopt "^4.0.1"
npm-packlist "^1.1.6"
npmlog "^4.0.2"
- rc "^1.1.7"
+ rc "^1.2.7"
rimraf "^2.6.1"
semver "^5.3.0"
tar "^4"
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
nwsapi@^2.0.0:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.3.tgz#3f4010d6c943f34018d3dfb5f2fbc0de90476959"
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.6.tgz#5fb7f5b828b97fe1de47eb2a6f8703036b6cb71a"
oauth-sign@~0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6"
object-keys@^1.0.11, object-keys@^1.0.8:
- version "1.0.11"
- resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d"
+ version "1.0.12"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2"
object-visit@^1.0.0:
version "1.0.1"
version "1.1.0"
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
-popper.js@^1.14.1:
+popper.js@^1.0.0:
version "1.14.3"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095"
supports-color "^3.2.3"
postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.13:
- version "6.0.22"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.22.tgz#e23b78314905c3b90cbd61702121e7a78848f2a3"
+ version "6.0.23"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
dependencies:
chalk "^2.4.1"
source-map "^0.6.1"
dependencies:
asap "~2.0.3"
-prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1:
- version "15.6.1"
- resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
+prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2:
+ version "15.6.2"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
dependencies:
- fbjs "^0.8.16"
loose-envify "^1.3.1"
object-assign "^4.1.1"
iconv-lite "0.4.19"
unpipe "1.0.0"
-rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
+rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
dependencies:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e"
react-jss@^8.1.0:
- version "8.5.1"
- resolved "https://registry.yarnpkg.com/react-jss/-/react-jss-8.5.1.tgz#f97c72f6a1c86aa6408932a2a2836ce40c0ab9fc"
+ version "8.6.1"
+ resolved "https://registry.yarnpkg.com/react-jss/-/react-jss-8.6.1.tgz#a06e2e1d2c4d91b4d11befda865e6c07fbd75252"
dependencies:
hoist-non-react-statics "^2.5.0"
jss "^9.7.0"
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
-react-popper@^0.10.0:
- version "0.10.4"
- resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.10.4.tgz#af2a415ea22291edd504678d7afda8a6ee3295aa"
- dependencies:
- popper.js "^1.14.1"
- prop-types "^15.6.1"
-
react-reconciler@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.7.0.tgz#9614894103e5f138deeeb5eabaf3ee80eb1d026d"
react-is "^16.4.1"
react-transition-group@^2.2.1:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.3.1.tgz#31d611b33e143a5e0f2d94c348e026a0f3b474b6"
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.4.0.tgz#1d9391fabfd82e016f26fabd1eec329dbd922b5a"
dependencies:
dom-helpers "^3.3.1"
loose-envify "^1.3.1"
- prop-types "^15.6.1"
+ prop-types "^15.6.2"
+ react-lifecycles-compat "^3.0.4"
react@16.4.1:
version "16.4.1"
set-immediate-shim "^1.0.1"
realpath-native@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.0.0.tgz#7885721a83b43bd5327609f0ddecb2482305fdf0"
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.0.1.tgz#07f40a0cce8f8261e2e8b7ebebf5c95965d7b633"
dependencies:
util.promisify "^1.0.0"
-"recompose@^0.26.0 || ^0.27.0":
+"recompose@^0.26.0 || ^0.27.0", recompose@^0.27.0:
version "0.27.1"
resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.27.1.tgz#1a49e931f183634516633bbb4f4edbfd3f38a7ba"
dependencies:
balanced-match "^0.4.2"
redux-devtools-instrument@^1.0.1:
- version "1.8.3"
- resolved "https://registry.yarnpkg.com/redux-devtools-instrument/-/redux-devtools-instrument-1.8.3.tgz#c510d67ab4e5e4525acd6e410c25ab46b85aca7c"
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/redux-devtools-instrument/-/redux-devtools-instrument-1.9.0.tgz#2faed9ac3292c783284b21843edfaa0567764a0c"
dependencies:
lodash "^4.2.0"
symbol-observable "^1.0.2"
version "1.4.0"
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
-regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.1:
+regenerator-runtime@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
+regenerator-runtime@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.0.tgz#8052ac952d85b10f3425192cd0c53f45cf65c6cb"
+
regenerator-transform@^0.10.0:
version "0.10.1"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"
path-parse "^1.0.5"
resolve@^1.1.7, resolve@^1.3.2:
- version "1.8.0"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.0.tgz#a7f2ac27b78480ecc09c83782741d9f26e4f0c3e"
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26"
dependencies:
path-parse "^1.0.5"
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
-safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"
tough-cookie@>=2.3.3, tough-cookie@^2.3.3:
- version "2.4.2"
- resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.2.tgz#aa9133154518b494efab98a58247bfc38818c00c"
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
dependencies:
psl "^1.1.24"
punycode "^1.4.1"
tsconfig-paths "^3.1.1"
tsconfig-paths@^3.1.1:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.4.0.tgz#d19fe80c5b245f99d17363471971eab54e65a8a7"
+ version "3.4.2"
+ resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.4.2.tgz#4640bffaeee3fc0ab986607edae203859156a8c3"
dependencies:
deepmerge "^2.0.1"
minimist "^1.2.0"
strip-json-comments "^2.0.1"
tslib@^1.8.0, tslib@^1.8.1:
- version "1.9.2"
- resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.2.tgz#8be0cc9a1f6dc7727c38deb16c2ebd1a2892988e"
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
tslint-config-prettier@^1.10.0:
version "1.13.0"
tsutils "^2.13.1"
tslint@^5.7.0:
- version "5.10.0"
- resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.10.0.tgz#11e26bccb88afa02dd0d9956cae3d4540b5f54c3"
+ version "5.11.0"
+ resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.11.0.tgz#98f30c02eae3cde7006201e4c33cb08b48581eed"
dependencies:
babel-code-frame "^6.22.0"
builtin-modules "^1.1.1"
resolve "^1.3.2"
semver "^5.3.0"
tslib "^1.8.0"
- tsutils "^2.12.1"
+ tsutils "^2.27.2"
-tsutils@^2.12.1, tsutils@^2.13.1:
- version "2.27.1"
- resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.27.1.tgz#ab0276ac23664f36ce8fd4414daec4aebf4373ee"
+tsutils@^2.13.1, tsutils@^2.27.2:
+ version "2.28.0"
+ resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.28.0.tgz#6bd71e160828f9d019b6f4e844742228f85169a1"
dependencies:
tslib "^1.8.1"
commander "~2.13.0"
source-map "~0.6.1"
-uglify-js@3.3.x:
- version "3.3.28"
- resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.3.28.tgz#0efb9a13850e11303361c1051f64d2ec68d9be06"
+uglify-js@3.4.x, uglify-js@^3.0.13:
+ version "3.4.5"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.5.tgz#650889c0766cf0f6fd5346cea09cd212f544be69"
dependencies:
- commander "~2.15.0"
+ commander "~2.16.0"
source-map "~0.6.1"
uglify-js@^2.6, uglify-js@^2.8.29:
optionalDependencies:
uglify-to-browserify "~1.0.0"
-uglify-js@^3.0.13:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.0.tgz#796762282b5b5f0eafe7d5c8c708d1d7bd5ba11d"
- dependencies:
- commander "~2.15.0"
- source-map "~0.6.1"
-
uglify-to-browserify@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
webpack-sources "^1.0.1"
uglifyjs-webpack-plugin@^1.1.8:
- version "1.2.5"
- resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.5.tgz#2ef8387c8f1a903ec5e44fa36f9f3cbdcea67641"
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.7.tgz#57638dd99c853a1ebfe9d97b42160a8a507f9d00"
dependencies:
cacache "^10.0.4"
find-cache-dir "^1.0.0"
crypto-random-string "^1.0.0"
universalify@^0.1.0:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7"
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
version "2.0.1"
resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
-upath@^1.0.0:
+upath@^1.0.5:
version "1.1.0"
resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd"
querystring "0.2.0"
use@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/use/-/use-3.1.0.tgz#14716bf03fdfefd03040aef58d8b4b85f3a7c544"
- dependencies:
- kind-of "^6.0.2"
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
uuid@^3.1.0:
- version "3.2.1"
- resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
validate-npm-package-license@^3.0.1:
version "3.0.3"