From: Pawel Kowalczyk Date: Mon, 23 Jul 2018 12:14:16 +0000 (+0200) Subject: merge conflicts X-Git-Tag: 1.2.0~33^2~7 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/10ce16c28de952f6533ca3cc9df909269e3d2a53?hp=b0de143e088973715681f7eb6c41f2dccb648c2b merge conflicts Feature #13781 Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk --- diff --git a/.env b/.env index 13aaad50..a523865a 100644 --- a/.env +++ b/.env @@ -2,4 +2,5 @@ # # 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 diff --git a/.licenseignore b/.licenseignore index c499a4f0..09914c90 100644 --- a/.licenseignore +++ b/.licenseignore @@ -10,3 +10,5 @@ cc-by-sa-3.0.txt README.md public/* .licenseignore +.yarnrc +.npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..cffe8cde --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +save-exact=true diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 00000000..95b8581e --- /dev/null +++ b/.yarnrc @@ -0,0 +1 @@ +save-prefix false diff --git a/Makefile b/Makefile index 30ab7bc4..c28b4b86 100644 --- a/Makefile +++ b/Makefile @@ -84,7 +84,7 @@ $(DEB_FILE): build --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 @@ -96,6 +96,7 @@ $(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) diff --git a/README.md b/README.md index 864a54fa..998d4246 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,22 @@ yarn install yarn build -### 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 diff --git a/etc/arvados/workbench2/workbench2.example.json b/etc/arvados/workbench2/workbench2.example.json new file mode 100644 index 00000000..d790112a --- /dev/null +++ b/etc/arvados/workbench2/workbench2.example.json @@ -0,0 +1,3 @@ +{ + "API_HOST": "CHANGE.TO.YOUR.ARVADOS.API.HOST" +} \ No newline at end of file diff --git a/package.json b/package.json index a8c56177..06fa893f 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,12 @@ "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", @@ -30,21 +30,21 @@ }, "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", diff --git a/src/common/api/common-resource-service.test.ts b/src/common/api/common-resource-service.test.ts index 7093b59c..83466245 100644 --- a/src/common/api/common-resource-service.test.ts +++ b/src/common/api/common-resource-service.test.ts @@ -2,7 +2,7 @@ // // 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"; diff --git a/src/common/api/common-resource-service.ts b/src/common/api/common-resource-service.ts index 39825c0e..3956fb73 100644 --- a/src/common/api/common-resource-service.ts +++ b/src/common/api/common-resource-service.ts @@ -3,8 +3,8 @@ // 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"; @@ -31,7 +31,7 @@ export interface Errors { errorToken: string; } -export default class CommonResourceService { +export class CommonResourceService { static mapResponseKeys = (response: any): Promise => CommonResourceService.mapKeys(_.camelCase)(response.data) @@ -103,6 +103,5 @@ export default class CommonResourceService { update(uuid: string) { throw new Error("Not implemented"); } - } diff --git a/src/common/api/filter-builder.test.ts b/src/common/api/filter-builder.test.ts index 34243934..d129a806 100644 --- a/src/common/api/filter-builder.test.ts +++ b/src/common/api/filter-builder.test.ts @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0 -import FilterBuilder from "./filter-builder"; +import { FilterBuilder } from "./filter-builder"; describe("FilterBuilder", () => { diff --git a/src/common/api/filter-builder.ts b/src/common/api/filter-builder.ts index 38c4fee8..28ad060f 100644 --- a/src/common/api/filter-builder.ts +++ b/src/common/api/filter-builder.ts @@ -5,8 +5,7 @@ import * as _ from "lodash"; import { Resource } from "../../models/resource"; -export default class FilterBuilder { - +export class FilterBuilder { static create(resourcePrefix = "") { return new FilterBuilder(resourcePrefix); } @@ -61,5 +60,4 @@ export default class FilterBuilder { } return this; } - } diff --git a/src/common/api/order-builder.test.ts b/src/common/api/order-builder.test.ts index b80756d4..f53bddb5 100644 --- a/src/common/api/order-builder.test.ts +++ b/src/common/api/order-builder.test.ts @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0 -import OrderBuilder from "./order-builder"; +import { OrderBuilder } from "./order-builder"; describe("OrderBuilder", () => { it("should build correct order query", () => { diff --git a/src/common/api/order-builder.ts b/src/common/api/order-builder.ts index b5a2e807..ed990541 100644 --- a/src/common/api/order-builder.ts +++ b/src/common/api/order-builder.ts @@ -5,14 +5,14 @@ import * as _ from "lodash"; import { Resource } from "../../models/resource"; -export default class OrderBuilder { +export class OrderBuilder { static create(prefix?: string){ return new OrderBuilder([], prefix); } private constructor( - private order: string[] = [], + private order: string[] = [], private prefix = ""){} private addRule (direction: string, attribute: keyof T) { diff --git a/src/common/api/server-api.ts b/src/common/api/server-api.ts index 330ce657..5beecd48 100644 --- a/src/common/api/server-api.ts +++ b/src/common/api/server-api.ts @@ -18,3 +18,7 @@ export function setServerApiAuthorizationHeader(token: string) { export function removeServerApiAuthorizationHeader() { delete serverApi.defaults.headers.common.Authorization; } + +export const setBaseUrl = (url: string) => { + serverApi.defaults.baseURL = url + "/arvados/v1"; +}; diff --git a/src/common/api/url-builder.ts b/src/common/api/url-builder.ts index e5786a23..0587c837 100644 --- a/src/common/api/url-builder.ts +++ b/src/common/api/url-builder.ts @@ -2,25 +2,25 @@ // // 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; + } } diff --git a/src/common/config.ts b/src/common/config.ts new file mode 100644 index 00000000..4b4a52a3 --- /dev/null +++ b/src/common/config.ts @@ -0,0 +1,23 @@ +// 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_URL + "?nocache=" + (new Date()).getTime()) + .then(response => response.data) + .catch(() => Promise.resolve(defaultConfig)); +}; + diff --git a/src/common/custom-theme.ts b/src/common/custom-theme.ts index 0850f881..c85acd90 100644 --- a/src/common/custom-theme.ts +++ b/src/common/custom-theme.ts @@ -8,6 +8,8 @@ import purple from '@material-ui/core/colors/purple'; 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; @@ -17,8 +19,16 @@ export interface ArvadosTheme extends Theme { 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: { @@ -26,9 +36,14 @@ const themeOptions: ArvadosThemeOptions = { } }, overrides: { + MuiTypography: { + body1: { + fontSize: '0.8125rem' + } + }, MuiAppBar: { colorPrimary: { - backgroundColor: purple900 + backgroundColor: purple800 } }, MuiTabs: { @@ -36,13 +51,28 @@ const themeOptions: ArvadosThemeOptions = { 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' } } }, diff --git a/src/common/formatters.ts b/src/common/formatters.ts index fe7df14c..38ef0223 100644 --- a/src/common/formatters.ts +++ b/src/common/formatters.ts @@ -10,7 +10,7 @@ export const formatDate = (isoDate: string) => { 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}`; } @@ -19,7 +19,7 @@ export const formatFileSize = (size?: number) => { return ""; }; -const fileSizes = [ +const FILE_SIZES = [ { base: 1000000000000, unit: "TB" diff --git a/src/common/url.ts b/src/common/url.ts new file mode 100644 index 00000000..1824f26a --- /dev/null +++ b/src/common/url.ts @@ -0,0 +1,6 @@ +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, ' ')); +} diff --git a/src/components/attribute/attribute.tsx b/src/components/attribute/attribute.tsx deleted file mode 100644 index ea35f5bf..00000000 --- a/src/components/attribute/attribute.tsx +++ /dev/null @@ -1,65 +0,0 @@ -// 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; - -class Attribute extends React.Component { - - hasLink() { - return !!this.props.link; - } - - render() { - const { label, link, value, children, classes } = this.props; - return - {label} - { this.hasLink() ? ( - {value} - ) : ( - - {value} - {children} - - )} - ; - } - -} - -type CssRules = 'attribute' | 'label' | 'value' | 'link'; - -const styles: StyleRulesCallback = (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 diff --git a/src/components/breadcrumbs/breadcrumbs.test.tsx b/src/components/breadcrumbs/breadcrumbs.test.tsx index ef3f8887..ea3d5ac2 100644 --- a/src/components/breadcrumbs/breadcrumbs.test.tsx +++ b/src/components/breadcrumbs/breadcrumbs.test.tsx @@ -6,7 +6,7 @@ import * as React from "react"; 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'; @@ -50,4 +50,4 @@ describe("", () => { }); -}); \ No newline at end of file +}); diff --git a/src/components/breadcrumbs/breadcrumbs.tsx b/src/components/breadcrumbs/breadcrumbs.tsx index 4868e137..da549dba 100644 --- a/src/components/breadcrumbs/breadcrumbs.tsx +++ b/src/components/breadcrumbs/breadcrumbs.tsx @@ -11,59 +11,52 @@ export interface Breadcrumb { label: string; } +type CssRules = "item" | "currentItem" | "label"; + +const styles: StyleRulesCallback = theme => ({ + item: { + opacity: 0.6 + }, + currentItem: { + opacity: 1 + }, + label: { + textTransform: "none" + } +}); + interface BreadcrumbsProps { items: Breadcrumb[]; onClick: (breadcrumb: Breadcrumb) => void; onContextMenu: (event: React.MouseEvent, breadcrumb: Breadcrumb) => void; } -const Breadcrumbs: React.SFC> = ({ classes, onClick, onContextMenu, items }) => { - return - { - items.map((item, index) => { - const isLastItem = index === items.length - 1; - return ( - - - - - { - !isLastItem && - } - - ); - }) - } - ; -}; - -type CssRules = "item" | "currentItem" | "label"; - -const styles: StyleRulesCallback = 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} + + + + {!isLastItem && } + + ); + }) + } + +); diff --git a/src/components/column-selector/column-selector.test.tsx b/src/components/column-selector/column-selector.test.tsx index c2835ad7..01dba85c 100644 --- a/src/components/column-selector/column-selector.test.tsx +++ b/src/components/column-selector/column-selector.test.tsx @@ -5,7 +5,7 @@ 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"; diff --git a/src/components/column-selector/column-selector.tsx b/src/components/column-selector/column-selector.tsx index b5dd43b8..0f496e25 100644 --- a/src/components/column-selector/column-selector.tsx +++ b/src/components/column-selector/column-selector.tsx @@ -3,19 +3,32 @@ // 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; onColumnToggle: (column: DataColumn) => void; } -const ColumnSelector: React.SFC> = ({ columns, onColumnToggle, classes }) => +type CssRules = "checkbox"; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + checkbox: { + width: 24, + height: 24 + } +}); + +export type ColumnSelectorProps = ColumnSelectorDataProps & WithStyles; + +export const ColumnSelector = withStyles(styles)( + ({ columns, onColumnToggle, classes }: ColumnSelectorProps) => @@ -38,20 +51,10 @@ const ColumnSelector: React.SFC> = ({ ))} - ; + +); -export const ColumnSelectorTrigger: React.SFC = (props) => +export const ColumnSelectorTrigger = (props: IconButtonProps) => ; - -type CssRules = "checkbox"; - -const styles: StyleRulesCallback = (theme: Theme) => ({ - checkbox: { - width: 24, - height: 24 - } -}); - -export default withStyles(styles)(ColumnSelector); diff --git a/src/components/context-menu/context-menu.test.tsx b/src/components/context-menu/context-menu.test.tsx index e4e2397d..a2452538 100644 --- a/src/components/context-menu/context-menu.test.tsx +++ b/src/components/context-menu/context-menu.test.tsx @@ -5,31 +5,32 @@ 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("", () => { - 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(); + 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 +}); diff --git a/src/components/context-menu/context-menu.tsx b/src/components/context-menu/context-menu.tsx index c892ba26..2103a2a0 100644 --- a/src/components/context-menu/context-menu.tsx +++ b/src/components/context-menu/context-menu.tsx @@ -4,25 +4,25 @@ 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 { +export interface ContextMenuProps { anchorEl?: HTMLElement; - actions: ContextMenuActionGroup[]; - onActionClick: (action: ContextMenuAction) => void; + items: ContextMenuItemGroup[]; + onItemClick: (action: ContextMenuItem) => void; onClose: () => void; } -export default class ContextMenu extends React.PureComponent> { +export class ContextMenu extends React.PureComponent { render() { - const { anchorEl, actions, onClose, onActionClick } = this.props; + const { anchorEl, items, onClose, onItemClick} = this.props; return extends React.PureComponent - {actions.map((group, groupIndex) => + {items.map((group, groupIndex) => - {group.map((action, actionIndex) => + {group.map((item, actionIndex) => onActionClick(action)}> + onClick={() => onItemClick(item)}> - + - {action.name} + {item.name} )} - {groupIndex < actions.length - 1 && } + {groupIndex < items.length - 1 && } )} ; diff --git a/src/components/data-explorer/data-explorer.test.tsx b/src/components/data-explorer/data-explorer.test.tsx index 5d4877f2..616a9c12 100644 --- a/src/components/data-explorer/data-explorer.test.tsx +++ b/src/components/data-explorer/data-explorer.test.tsx @@ -6,10 +6,10 @@ import * as React from "react"; 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() }); @@ -109,4 +109,4 @@ const mockDataExplorerProps = () => ({ onChangePage: jest.fn(), onChangeRowsPerPage: jest.fn(), onContextMenu: jest.fn() -}); \ No newline at end of file +}); diff --git a/src/components/data-explorer/data-explorer.tsx b/src/components/data-explorer/data-explorer.tsx index e851ca99..4699fd6d 100644 --- a/src/components/data-explorer/data-explorer.tsx +++ b/src/components/data-explorer/data-explorer.tsx @@ -3,15 +3,27 @@ // 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 { +type CssRules = "searchBox" | "toolbar"; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + searchBox: { + paddingBottom: theme.spacing.unit * 2 + }, + toolbar: { + paddingTop: theme.spacing.unit * 2 + } +}); + +interface DataExplorerDataProps { items: T[]; itemsAvailable: number; columns: DataColumns; @@ -31,33 +43,35 @@ interface DataExplorerProps { extractKey?: (item: T) => React.Key; } -class DataExplorer extends React.Component & WithStyles> { +type DataExplorerProps = DataExplorerDataProps & WithStyles; - render() { - return - - -
- -
- -
-
- 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} /> - - {this.props.items.length > 0 && +export const DataExplorer = withStyles(styles)( + class DataExplorerGeneric extends React.Component> { + render() { + return + + +
+ +
+ +
+
+ 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}/> + + {this.props.items.length > 0 && extends React.Component & WithStyles< component="div" /> } - -
; - } +
+
; + } - changePage = (event: React.MouseEvent | null, page: number) => { - this.props.onChangePage(page); - } + changePage = (event: React.MouseEvent, page: number) => { + this.props.onChangePage(page); + } - changeRowsPerPage: React.ChangeEventHandler = (event) => { - this.props.onChangeRowsPerPage(parseInt(event.target.value, 10)); - } + changeRowsPerPage: React.ChangeEventHandler = (event) => { + this.props.onChangeRowsPerPage(parseInt(event.target.value, 10)); + } - renderContextMenuTrigger = (item: T) => - - this.props.onContextMenu(event, item)}> - - - + renderContextMenuTrigger = (item: T) => + + this.props.onContextMenu(event, item)}> + + + - contextMenuColumn = { - name: "Actions", - selected: true, - key: "context-actions", - renderHeader: () => null, - render: this.renderContextMenuTrigger, - width: "auto" - }; - -} - -type CssRules = "searchBox" | "toolbar"; - -const styles: StyleRulesCallback = (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); +); diff --git a/src/components/data-table-filters/data-table-filters.test.tsx b/src/components/data-table-filters/data-table-filters.test.tsx index b2daebef..b0a84b51 100644 --- a/src/components/data-table-filters/data-table-filters.test.tsx +++ b/src/components/data-table-filters/data-table-filters.test.tsx @@ -4,7 +4,7 @@ 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"; @@ -19,12 +19,12 @@ describe("", () => { name: "Filter 2", selected: false }]; - const dataTableFilter = mount(); + const dataTableFilter = mount(); 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", @@ -34,7 +34,7 @@ describe("", () => { name: "Filter 2", selected: true }]; - const dataTableFilter = mount(); + const dataTableFilter = mount(); dataTableFilter.find(ButtonBase).simulate("click"); expect(dataTableFilter.find(Checkbox).prop("checked")).toBeTruthy(); dataTableFilter.find(ListItem).simulate("click"); @@ -53,7 +53,7 @@ describe("", () => { selected: false }]; const onChange = jest.fn(); - const dataTableFilter = mount(); + const dataTableFilter = mount(); dataTableFilter.find(ButtonBase).simulate("click"); dataTableFilter.find(ListItem).at(1).simulate("click"); dataTableFilter.find(Button).at(0).simulate("click"); @@ -65,4 +65,4 @@ describe("", () => { selected: true }]); }); -}); \ No newline at end of file +}); diff --git a/src/components/data-table-filters/data-table-filters.tsx b/src/components/data-table-filters/data-table-filters.tsx index bede5aea..d288a5a3 100644 --- a/src/components/data-table-filters/data-table-filters.tsx +++ b/src/components/data-table-filters/data-table-filters.tsx @@ -23,133 +23,6 @@ import { 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, DataTableFilterState> { - state: DataTableFilterState = { - anchorEl: undefined, - filters: [], - prevFilters: [] - }; - icon = React.createRef(); - - render() { - const { name, classes, children } = this.props; - const isActive = this.state.filters.some(f => f.selected); - return <> - - {children} - - - - - - - {name} - - - - {this.state.filters.map((filter, index) => - - - - {filter.name} - - - )} - - - - - - - - ; - } - - 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 = (theme: Theme) => ({ @@ -185,4 +58,130 @@ const styles: StyleRulesCallback = (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, DataTableFilterState> { + state: DataTableFilterState = { + anchorEl: undefined, + filters: [], + prevFilters: [] + }; + icon = React.createRef(); + + render() { + const { name, classes, children } = this.props; + const isActive = this.state.filters.some(f => f.selected); + return <> + + {children} + + + + + + + {name} + + + + {this.state.filters.map((filter, index) => + + + + {filter.name} + + + )} + + + + + + + + ; + } + + 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) + })); + } + } +); diff --git a/src/components/data-table/data-table.test.tsx b/src/components/data-table/data-table.test.tsx index ec84acac..7e460c89 100644 --- a/src/components/data-table/data-table.test.tsx +++ b/src/components/data-table/data-table.test.tsx @@ -6,8 +6,8 @@ import * as React from "react"; 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() }); @@ -169,6 +169,4 @@ describe("", () => { dataTable.find(DataTableFilters).prop("onChange")([]); expect(onFiltersChange).toHaveBeenCalledWith([], columns[0]); }); - - -}); \ No newline at end of file +}); diff --git a/src/components/data-table/data-table.tsx b/src/components/data-table/data-table.tsx index e0e30480..829bc84e 100644 --- a/src/components/data-table/data-table.tsx +++ b/src/components/data-table/data-table.tsx @@ -5,11 +5,11 @@ 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 = Array>; -export interface DataTableProps { +export interface DataTableDataProps { items: T[]; columns: DataColumns; onRowClick: (event: React.MouseEvent, item: T) => void; @@ -20,80 +20,6 @@ export interface DataTableProps { extractKey?: (item: T) => React.Key; } -class DataTable extends React.Component & WithStyles> { - render() { - const { items, classes } = this.props; - return
- - - - {this.mapVisibleColumns(this.renderHeadCell)} - - - - {items.map(this.renderBodyRow)} - -
-
; - } - - renderHeadCell = (column: DataColumn, index: number) => { - const { name, key, renderHeader, filters, sortDirection } = column; - const { onSortToggle, onFiltersChange } = this.props; - return - {renderHeader ? - renderHeader() : - filters - ? - onFiltersChange && - onFiltersChange(filters, column)} - filters={filters}> - {name} - - : sortDirection - ? - onSortToggle && - onSortToggle(column)}> - {name} - - : - {name} - } - ; - } - - renderBodyRow = (item: T, index: number) => { - const { onRowClick, onRowDoubleClick, extractKey } = this.props; - return onRowClick && onRowClick(event, item)} - onContextMenu={this.handleRowContextMenu(item)} - onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item) }> - {this.mapVisibleColumns((column, index) => ( - - {column.render(item)} - - ))} - ; - } - - mapVisibleColumns = (fn: (column: DataColumn, index: number) => React.ReactElement) => { - return this.props.columns.filter(column => column.selected).map(fn); - } - - handleRowContextMenu = (item: T) => - (event: React.MouseEvent) => - this.props.onContextMenu(event, item) - -} - type CssRules = "tableBody" | "tableContainer" | "noItemsInfo"; const styles: StyleRulesCallback = (theme: Theme) => ({ @@ -110,4 +36,80 @@ const styles: StyleRulesCallback = (theme: Theme) => ({ } }); -export default withStyles(styles)(DataTable); +type DataTableProps = DataTableDataProps & WithStyles; + +export const DataTable = withStyles(styles)( + class Component extends React.Component> { + render() { + const { items, classes } = this.props; + return
+ + + + {this.mapVisibleColumns(this.renderHeadCell)} + + + + {items.map(this.renderBodyRow)} + +
+
; + } + + renderHeadCell = (column: DataColumn, index: number) => { + const { name, key, renderHeader, filters, sortDirection } = column; + const { onSortToggle, onFiltersChange } = this.props; + return + {renderHeader ? + renderHeader() : + filters + ? + onFiltersChange && + onFiltersChange(filters, column)} + filters={filters}> + {name} + + : sortDirection + ? + onSortToggle && + onSortToggle(column)}> + {name} + + : + {name} + } + ; + } + + renderBodyRow = (item: T, index: number) => { + const { onRowClick, onRowDoubleClick, extractKey } = this.props; + return onRowClick && onRowClick(event, item)} + onContextMenu={this.handleRowContextMenu(item)} + onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item) }> + {this.mapVisibleColumns((column, index) => ( + + {column.render(item)} + + ))} + ; + } + + mapVisibleColumns = (fn: (column: DataColumn, index: number) => React.ReactElement) => { + return this.props.columns.filter(column => column.selected).map(fn); + } + + handleRowContextMenu = (item: T) => + (event: React.MouseEvent) => + this.props.onContextMenu(event, item) + + } +); diff --git a/src/components/details-attribute/details-attribute.tsx b/src/components/details-attribute/details-attribute.tsx new file mode 100644 index 00000000..56da6c17 --- /dev/null +++ b/src/components/details-attribute/details-attribute.tsx @@ -0,0 +1,55 @@ +// 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 = (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; + +export const DetailsAttribute = withStyles(styles)(({ label, link, value, children, classes }: DetailsAttributeProps) => + + {label} + { link + ? {value} + : + {value} + {children} + } + +); diff --git a/src/components/details-panel-factory/details-panel-factory.tsx b/src/components/details-panel-factory/details-panel-factory.tsx deleted file mode 100644 index bb7d8556..00000000 --- a/src/components/details-panel-factory/details-panel-factory.tsx +++ /dev/null @@ -1,27 +0,0 @@ -// 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 diff --git a/src/components/details-panel-factory/items/abstract-item.tsx b/src/components/details-panel-factory/items/abstract-item.tsx deleted file mode 100644 index a50c867b..00000000 --- a/src/components/details-panel-factory/items/abstract-item.tsx +++ /dev/null @@ -1,23 +0,0 @@ -// 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 { - - constructor(protected item: T) {} - - getTitle(): string { - return this.item.name; - } - - abstract getIcon(): IconTypes; - abstract buildDetails(): React.ReactElement; - - buildActivity(): React.ReactElement { - return
; - } -} \ No newline at end of file diff --git a/src/components/details-panel-factory/items/collection-item.tsx b/src/components/details-panel-factory/items/collection-item.tsx deleted file mode 100644 index 1fa28919..00000000 --- a/src/components/details-panel-factory/items/collection-item.tsx +++ /dev/null @@ -1,33 +0,0 @@ -// 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 { - - getIcon(): IconTypes { - return IconTypes.COLLECTION; - } - - buildDetails(): React.ReactElement { - return
- - - - - - {/* Links but we dont have view */} - - - {/* Missing attrs */} - - -
; - } -} \ No newline at end of file diff --git a/src/components/details-panel-factory/items/empty-item.tsx b/src/components/details-panel-factory/items/empty-item.tsx deleted file mode 100644 index 16394c89..00000000 --- a/src/components/details-panel-factory/items/empty-item.tsx +++ /dev/null @@ -1,21 +0,0 @@ -// 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 { - - getIcon(): IconTypes { - return IconTypes.FOLDER; - } - - buildDetails(): React.ReactElement { - return ; - } -} \ No newline at end of file diff --git a/src/components/details-panel-factory/items/process-item.tsx b/src/components/details-panel-factory/items/process-item.tsx deleted file mode 100644 index 1ea34dee..00000000 --- a/src/components/details-panel-factory/items/process-item.tsx +++ /dev/null @@ -1,43 +0,0 @@ -// 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 { - - getIcon(): IconTypes { - return IconTypes.PROCESS; - } - - buildDetails(): React.ReactElement { - return
- - - - - {/* Missing attr */} - - - - {/* Missing attrs */} - - - - {/* Links but we dont have view */} - - - - - - - {/* Link but we dont have view */} - -
; - } -} \ No newline at end of file diff --git a/src/components/details-panel-factory/items/project-item.tsx b/src/components/details-panel-factory/items/project-item.tsx deleted file mode 100644 index 559816e1..00000000 --- a/src/components/details-panel-factory/items/project-item.tsx +++ /dev/null @@ -1,31 +0,0 @@ -// 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 { - - getIcon(): IconTypes { - return IconTypes.FOLDER; - } - - buildDetails(): React.ReactElement { - return
- - {/* Missing attr */} - - - - - {/* Missing attr */} - - -
; - } -} \ No newline at end of file diff --git a/src/components/dropdown-menu/dropdown-menu.test.tsx b/src/components/dropdown-menu/dropdown-menu.test.tsx index 19924ad7..da232bd2 100644 --- a/src/components/dropdown-menu/dropdown-menu.test.tsx +++ b/src/components/dropdown-menu/dropdown-menu.test.tsx @@ -4,23 +4,22 @@ 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("", () => { it("renders menu icon", () => { - const dropdownMenu = shallow(); - expect(dropdownMenu.find(ChevronRightIcon)).toHaveLength(1); + const dropdownMenu = shallow(} />); + expect(dropdownMenu.find(PaginationRightArrowIcon)).toHaveLength(1); }); it("render menu items", () => { const dropdownMenu = shallow( - + }> Item 1 Item 2 @@ -29,15 +28,15 @@ describe("", () => { }); it("opens on menu icon click", () => { - const dropdownMenu = shallow(); + const dropdownMenu = shallow(} />); dropdownMenu.find(IconButton).simulate("click", {currentTarget: {}}); expect(dropdownMenu.state().anchorEl).toBeDefined(); }); - + it("closes on menu click", () => { - const dropdownMenu = shallow(); + const dropdownMenu = shallow(} />); dropdownMenu.find(Menu).simulate("click", {currentTarget: {}}); expect(dropdownMenu.state().anchorEl).toBeUndefined(); }); -}); \ No newline at end of file +}); diff --git a/src/components/dropdown-menu/dropdown-menu.tsx b/src/components/dropdown-menu/dropdown-menu.tsx index 4f2b83af..73b279b2 100644 --- a/src/components/dropdown-menu/dropdown-menu.tsx +++ b/src/components/dropdown-menu/dropdown-menu.tsx @@ -3,17 +3,20 @@ // 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; } -class DropdownMenu extends React.Component { +interface DropdownMenuState { + anchorEl: any; +} +export class DropdownMenu extends React.Component { state = { anchorEl: undefined }; @@ -24,7 +27,7 @@ class DropdownMenu extends React.Component { }; render() { - const { icon: Icon, id, children } = this.props; + const { icon, id, children } = this.props; const { anchorEl } = this.state; return (
@@ -32,10 +35,8 @@ class DropdownMenu extends React.Component { aria-owns={anchorEl ? id : undefined} aria-haspopup="true" color="inherit" - onClick={this.handleOpen} - - > - + onClick={this.handleOpen}> + {icon} { onClose={this.handleClose} onClick={this.handleClose} anchorOrigin={this.transformOrigin} - transformOrigin={this.transformOrigin} - > + transformOrigin={this.transformOrigin}> {children}
@@ -60,6 +60,3 @@ class DropdownMenu extends React.Component { this.setState({ anchorEl: event.currentTarget }); } } - - -export default DropdownMenu; diff --git a/src/components/empty-state/empty-state.tsx b/src/components/empty-state/empty-state.tsx deleted file mode 100644 index 205053b5..00000000 --- a/src/components/empty-state/empty-state.tsx +++ /dev/null @@ -1,46 +0,0 @@ -// 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; - -class EmptyState extends React.Component { - - render() { - const { classes, message, details, icon, children } = this.props; - return ( - - - {message} - { details && {details} } - { children && {children} } - - ); - } - -} - -type CssRules = 'container' | 'icon'; -const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ - container: { - textAlign: 'center' - }, - icon: { - color: theme.palette.grey["500"], - fontSize: '72px' - } -}); - -export default withStyles(styles)(EmptyState); \ No newline at end of file diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx index c420a19c..e80fee8e 100644 --- a/src/components/icon/icon.tsx +++ b/src/components/icon/icon.tsx @@ -3,48 +3,71 @@ // 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: , - folder: , - close: , - project: , - collection: , - process: -}); - -class IconBase extends React.Component { - 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) => ; +export const AdvancedIcon: IconType = (props) => ; +export const CustomizeTableIcon: IconType = (props) => ; +export const CopyIcon: IconType = (props) => ; +export const CollectionIcon: IconType = (props) => ; +export const CloseIcon: IconType = (props) => ; +export const DefaultIcon: IconType = (props) => ; +export const DetailsIcon: IconType = (props) => ; +export const DownloadIcon: IconType = (props) => ; +export const FavoriteIcon: IconType = (props) => ; +export const HelpIcon: IconType = (props) => ; +export const LogIcon: IconType = (props) => ; +export const MoreOptionsIcon: IconType = (props) => ; +export const MoveToIcon: IconType = (props) => ; +export const NewProjectIcon: IconType = (props) => ; +export const NotificationIcon: IconType = (props) => ; +export const PaginationDownIcon: IconType = (props) => ; +export const PaginationLeftArrowIcon: IconType = (props) => ; +export const PaginationRightArrowIcon: IconType = (props) => ; +export const ProcessIcon: IconType = (props) => ; +export const ProjectIcon: IconType = (props) => ; +export const ProjectsIcon: IconType = (props) => ; +export const ProvenanceGraphIcon: IconType = (props) => ; +export const RecentIcon: IconType = (props) => ; +export const RemoveIcon: IconType = (props) => ; +export const RemoveFavoriteIcon: IconType = (props) => ; +export const RenameIcon: IconType = (props) => ; +export const ReRunProcessIcon: IconType = (props) => ; +export const SearchIcon: IconType = (props) => ; +export const ShareIcon: IconType = (props) => ; +export const ShareMeIcon: IconType = (props) => ; +export const SidePanelRightArrowIcon: IconType = (props) => ; +export const TrashIcon: IconType = (props) => ; +export const UserPanelIcon: IconType = (props) => ; +export const UsedByIcon: IconType = (props) => ; +export const WorkflowIcon: IconType = (props) => ; \ No newline at end of file diff --git a/src/components/list-item-text-icon/list-item-text-icon.tsx b/src/components/list-item-text-icon/list-item-text-icon.tsx new file mode 100644 index 00000000..8f9d4744 --- /dev/null +++ b/src/components/list-item-text-icon/list-item-text-icon.tsx @@ -0,0 +1,62 @@ +// 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 = (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; + +export const ListItemTextIcon = withStyles(styles)( + class extends React.Component { + render() { + const { classes, isActive, hasMargin, name, icon: Icon } = this.props; + return ( + + + + + + {name} + + } /> + + ); + } + } +); diff --git a/src/components/popover/helpers.ts b/src/components/popover/helpers.ts index 13f74a68..f2be98cf 100644 --- a/src/components/popover/helpers.ts +++ b/src/components/popover/helpers.ts @@ -4,13 +4,13 @@ import { PopoverOrigin } from "@material-ui/core/Popover"; -export const mockAnchorFromMouseEvent = (event: React.MouseEvent) => { +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 }; diff --git a/src/components/popover/popover.test.tsx b/src/components/popover/popover.test.tsx index fa24c0cd..37007ab4 100644 --- a/src/components/popover/popover.test.tsx +++ b/src/components/popover/popover.test.tsx @@ -6,7 +6,7 @@ import * as React from "react"; 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() }); @@ -38,7 +38,7 @@ describe("", () => { popover.find(DefaultTrigger).simulate("click"); expect(popover.find(CustomTrigger)).toHaveLength(1); }); - + it("does not close if closeOnContentClick is not set", () => { const popover = mount( @@ -66,4 +66,4 @@ const CustomTrigger: React.SFC = (props) => ( -); \ No newline at end of file +); diff --git a/src/components/popover/popover.tsx b/src/components/popover/popover.tsx index c8d40338..9f3cd780 100644 --- a/src/components/popover/popover.tsx +++ b/src/components/popover/popover.tsx @@ -13,9 +13,7 @@ export interface PopoverProps { closeOnContentClick?: boolean; } - -class Popover extends React.Component { - +export class Popover extends React.Component { state = { anchorEl: undefined }; @@ -57,7 +55,6 @@ class Popover extends React.Component { this.handleClose(); } } - } export const DefaultTrigger: React.SFC = (props) => ( @@ -65,5 +62,3 @@ export const DefaultTrigger: React.SFC = (props) => ( ); - -export default Popover; diff --git a/src/components/search-bar/search-bar.test.tsx b/src/components/search-bar/search-bar.test.tsx index 2479e401..07b5ebf6 100644 --- a/src/components/search-bar/search-bar.test.tsx +++ b/src/components/search-bar/search-bar.test.tsx @@ -4,7 +4,7 @@ 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'; @@ -61,7 +61,7 @@ describe("", () => { 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.find("input").simulate("change", { target: { value: "current value" } }); @@ -70,7 +70,7 @@ describe("", () => { 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.find("input").simulate("change", { target: { value: "current value" } }); @@ -79,7 +79,7 @@ describe("", () => { jest.advanceTimersByTime(1000); expect(onSearch).toHaveBeenCalledTimes(1); }); - + it("calls onSearch again after the specified time has passed since previous call", () => { const searchBar = mount(); searchBar.find("input").simulate("change", { target: { value: "current value" } }); @@ -91,9 +91,7 @@ describe("", () => { jest.advanceTimersByTime(1000); expect(onSearch).toBeCalledWith("latest value"); expect(onSearch).toHaveBeenCalledTimes(2); - - }); + }); }); - -}); \ No newline at end of file +}); diff --git a/src/components/search-bar/search-bar.tsx b/src/components/search-bar/search-bar.tsx index 62c8cc3c..de9e7de5 100644 --- a/src/components/search-bar/search-bar.tsx +++ b/src/components/search-bar/search-bar.tsx @@ -6,80 +6,6 @@ import * as React from 'react'; 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; - -interface SearchBarState { - value: string; -} - -export const DEFAULT_SEARCH_DEBOUNCE = 1000; - -class SearchBar extends React.Component { - - state: SearchBarState = { - value: "" - }; - - timeout: number; - - render() { - const { classes } = this.props; - return -
- - - - -
-
; - } - - 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) => { - event.preventDefault(); - clearTimeout(this.timeout); - this.props.onSearch(this.state.value); - } - - handleChange = (event: React.ChangeEvent) => { - 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 = theme => { @@ -106,4 +32,76 @@ const styles: StyleRulesCallback = 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; + +interface SearchBarState { + value: string; +} + +export const DEFAULT_SEARCH_DEBOUNCE = 1000; + +export const SearchBar = withStyles(styles)( + class extends React.Component { + state: SearchBarState = { + value: "" + }; + + timeout: number; + + render() { + const {classes} = this.props; + return +
+ + + + +
+
; + } + + 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) => { + event.preventDefault(); + clearTimeout(this.timeout); + this.props.onSearch(this.state.value); + } + + handleChange = (event: React.ChangeEvent) => { + 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 + ); + + } + } +); diff --git a/src/components/search-input/search-input.test.tsx b/src/components/search-input/search-input.test.tsx index b07445a5..a91f9b17 100644 --- a/src/components/search-input/search-input.test.tsx +++ b/src/components/search-input/search-input.test.tsx @@ -4,7 +4,7 @@ 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'; @@ -61,7 +61,7 @@ describe("", () => { 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.find("input").simulate("change", { target: { value: "current value" } }); @@ -70,7 +70,7 @@ describe("", () => { 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.find("input").simulate("change", { target: { value: "current value" } }); @@ -79,7 +79,7 @@ describe("", () => { jest.advanceTimersByTime(1000); expect(onSearch).toHaveBeenCalledTimes(1); }); - + it("calls onSearch again after the specified time has passed since previous call", () => { const searchInput = mount(); searchInput.find("input").simulate("change", { target: { value: "current value" } }); @@ -91,9 +91,9 @@ describe("", () => { jest.advanceTimersByTime(1000); expect(onSearch).toBeCalledWith("latest value"); expect(onSearch).toHaveBeenCalledTimes(2); - + }); }); -}); \ No newline at end of file +}); diff --git a/src/components/search-input/search-input.tsx b/src/components/search-input/search-input.tsx index edc82d55..dc02cd3d 100644 --- a/src/components/search-input/search-input.tsx +++ b/src/components/search-input/search-input.tsx @@ -3,87 +3,9 @@ // 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; - -interface SearchInputState { - value: string; -} - -export const DEFAULT_SEARCH_DEBOUNCE = 1000; - -class SearchInput extends React.Component { - - state: SearchInputState = { - value: "" - }; - - timeout: number; - - render() { - const { classes } = this.props; - return
- - Search - - - - - - } /> - -
; - } - - 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) => { - event.preventDefault(); - clearTimeout(this.timeout); - this.props.onSearch(this.state.value); - } - - handleChange = (event: React.ChangeEvent) => { - 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 = theme => { @@ -110,4 +32,80 @@ const styles: StyleRulesCallback = 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; + +interface SearchInputState { + value: string; +} + +export const DEFAULT_SEARCH_DEBOUNCE = 1000; + +export const SearchInput = withStyles(styles)( + class extends React.Component { + state: SearchInputState = { + value: "" + }; + + timeout: number; + + render() { + const { classes } = this.props; + return
+ + Search + + + + + + }/> + +
; + } + + 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) => { + event.preventDefault(); + clearTimeout(this.timeout); + this.props.onSearch(this.state.value); + } + + handleChange = (event: React.ChangeEvent) => { + 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 + ); + + } + } +); diff --git a/src/components/side-panel/side-panel.tsx b/src/components/side-panel/side-panel.tsx index a7783fb2..4240b1bf 100644 --- a/src/components/side-panel/side-panel.tsx +++ b/src/components/side-panel/side-panel.tsx @@ -4,116 +4,119 @@ 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 = (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, item: SidePanelItem) => void; } -class SidePanel extends React.Component> { - render(): ReactElement { - const { classes, toggleOpen, toggleActive, sidePanelItems, children } = this.props; - const { listItemText, leftSidePanelContainer, row, list, icon, projectIconMargin, active, activeArrow, inactiveArrow, arrowTransition, arrowRotate } = classes; - return ( -
- - {sidePanelItems.map(it => ( - - toggleActive(it.id)} onContextMenu={this.handleRowContextMenu(it)}> - - {it.openAble ? toggleOpen(it.id)} className={`${it.active ? activeArrow : inactiveArrow} - ${it.open ? `fas fa-caret-down ${arrowTransition}` : `fas fa-caret-down ${arrowRotate}`}`} /> : null} - - - - {it.name}} /> - - - {it.openAble ? ( - - {children} - ) : null} - - ))} - -
- ); - } - - handleRowContextMenu = (item: SidePanelItem) => - (event: React.MouseEvent) => - item.openAble ? this.props.onContextMenu(event, item) : null +type SidePanelProps = SidePanelDataProps & WithStyles; -} +export const SidePanel = withStyles(styles)( + class extends React.Component { + render(): ReactElement { + const { classes, toggleOpen, toggleActive, sidePanelItems, children } = this.props; + const { root, row, list, toggableIconContainer } = classes; + return ( +
+ + {sidePanelItems.map(it => ( + + toggleActive(it.id)} + onContextMenu={this.handleRowContextMenu(it)}> + + {it.openAble ? ( + toggleOpen(it.id)} className={toggableIconContainer}> + + < SidePanelRightArrowIcon/> + + + ) : null} + + + + {it.openAble ? ( + + {children} + + ) : null} + + ))} + +
+ ); + } -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 = (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) => + item.openAble ? this.props.onContextMenu(event, item) : null } -}); - -export default withStyles(styles)(SidePanel); \ No newline at end of file +); diff --git a/src/components/tree/tree.test.tsx b/src/components/tree/tree.test.tsx index 9ac05113..58484c37 100644 --- a/src/components/tree/tree.test.tsx +++ b/src/components/tree/tree.test.tsx @@ -7,7 +7,7 @@ import * as Enzyme from 'enzyme'; 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'; diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx index 8de9bda5..e4d8c72c 100644 --- a/src/components/tree/tree.tsx +++ b/src/components/tree/tree.tsx @@ -5,10 +5,43 @@ 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 = (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, @@ -35,76 +68,48 @@ interface TreeProps { onContextMenu: (event: React.MouseEvent, item: TreeItem) => void; } -class Tree extends React.Component & WithStyles, {}> { - render(): ReactElement { - 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 - {items && items.map((it: TreeItem, idx: number) => -
- toggleItemActive(it.id, it.status)} onContextMenu={this.handleRowContextMenu(it)}> - {it.status === TreeItemStatus.Pending ? : 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)} - - {it.items && it.items.length > 0 && +export const Tree = withStyles(styles)( + class Component extends React.Component & WithStyles, {}> { + render(): ReactElement { + 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 + {items && items.map((it: TreeItem, idx: number) => +
+ toggleItemActive(it.id, it.status)} + onContextMenu={this.handleRowContextMenu(it)}> + {it.status === TreeItemStatus.Pending ? + : 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)} + + {it.items && it.items.length > 0 && - + onContextMenu={onContextMenu}/> } -
)} -
; - } - renderArrow(status: TreeItemStatus, arrowClass: string, open: boolean, id: string) { - const { arrowTransition, arrowVisibility, arrowRotate } = this.props.classes; - return 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) => - (event: React.MouseEvent) => - this.props.onContextMenu(event, item) -} +
)} +
; + } -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 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 = (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) => + (event: React.MouseEvent) => + this.props.onContextMenu(event, item) } -}); - -const StyledTree = withStyles(styles)(Tree); -export default StyledTree; +); diff --git a/src/index.tsx b/src/index.tsx index a06b4851..6d53e0d4 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,51 +5,54 @@ 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(getProjectList(authService.getUuid())); - -// const service = new CommonResourceService(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(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 = () => - - - -
- - -
-
-
-
; - -ReactDOM.render( - , - 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(getProjectList(authService.getUuid())); + + const App = () => + + + +
+ + +
+
+
+
; + + ReactDOM.render( + , + document.getElementById('root') as HTMLElement + ); + }); + + diff --git a/src/models/container-request.ts b/src/models/container-request.ts index 162e0f57..4e548157 100644 --- a/src/models/container-request.ts +++ b/src/models/container-request.ts @@ -35,5 +35,4 @@ export interface ContainerRequestResource extends Resource { logUuid: string; outputUuid: string; filters: string; - -} \ No newline at end of file +} diff --git a/src/models/details.ts b/src/models/details.ts new file mode 100644 index 00000000..42eb5c9c --- /dev/null +++ b/src/models/details.ts @@ -0,0 +1,10 @@ +// 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; diff --git a/src/models/empty.ts b/src/models/empty.ts index 9731207f..539f9f57 100644 --- a/src/models/empty.ts +++ b/src/models/empty.ts @@ -2,9 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { ResourceKind } from "./resource"; - export interface EmptyResource { name: string; kind: undefined; -} \ No newline at end of file +} diff --git a/src/services/auth-service/auth-service.ts b/src/services/auth-service/auth-service.ts index e953a75d..1879e6a0 100644 --- a/src/services/auth-service/auth-service.ts +++ b/src/services/auth-service/auth-service.ts @@ -2,8 +2,9 @@ // // 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'; @@ -21,7 +22,9 @@ export interface UserDetailsResponse { is_admin: boolean; } -export default class AuthService { +export class AuthService { + + constructor(protected serverApi: AxiosInstance) { } public saveApiToken(token: string) { localStorage.setItem(API_TOKEN_KEY, token); @@ -82,7 +85,7 @@ export default class AuthService { } public getUserDetails = (): Promise => { - return serverApi + return this.serverApi .get('/users/current') .then(resp => ({ email: resp.data.email, diff --git a/src/services/groups-service/groups-service.test.ts b/src/services/groups-service/groups-service.test.ts index 2562a595..c3be8bda 100644 --- a/src/services/groups-service/groups-service.test.ts +++ b/src/services/groups-service/groups-service.test.ts @@ -4,7 +4,7 @@ import axios from "axios"; import MockAdapter from "axios-mock-adapter"; -import GroupsService from "./groups-service"; +import { GroupsService } from "./groups-service"; describe("GroupsService", () => { diff --git a/src/services/groups-service/groups-service.ts b/src/services/groups-service/groups-service.ts index 1318dace..dfaf11d6 100644 --- a/src/services/groups-service/groups-service.ts +++ b/src/services/groups-service/groups-service.ts @@ -3,9 +3,9 @@ // 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"; @@ -25,7 +25,7 @@ export type GroupContentsResource = ProjectResource | ProcessResource; -export default class GroupsService extends CommonResourceService { +export class GroupsService extends CommonResourceService { constructor(serverApi: AxiosInstance) { super(serverApi, "groups"); @@ -50,4 +50,4 @@ export enum GroupContentsResourcePrefix { Collection = "collections", Project = "groups", Process = "container_requests" -} \ No newline at end of file +} diff --git a/src/services/project-service/project-service.test.ts b/src/services/project-service/project-service.test.ts index 68df2450..f915c2df 100644 --- a/src/services/project-service/project-service.test.ts +++ b/src/services/project-service/project-service.test.ts @@ -3,9 +3,8 @@ // 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", () => { @@ -35,5 +34,5 @@ describe("CommonResourceService", () => { } }); }); - + }); diff --git a/src/services/project-service/project-service.ts b/src/services/project-service/project-service.ts index 9ce9e213..f759547a 100644 --- a/src/services/project-service/project-service.ts +++ b/src/services/project-service/project-service.ts @@ -2,13 +2,13 @@ // // 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 { +export class ProjectService extends GroupsService { create(data: Partial) { const projectData = { ...data, groupClass: GroupClass.Project }; @@ -33,4 +33,4 @@ export default class ProjectService extends GroupsService { .addEqual("groupClass", GroupClass.Project)); } -} \ No newline at end of file +} diff --git a/src/services/services.ts b/src/services/services.ts index 143e97bd..57f07d6c 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2,11 +2,11 @@ // // 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); diff --git a/src/store/auth/auth-action.ts b/src/store/auth/auth-action.ts index a6e6f797..e9930a02 100644 --- a/src/store/auth/auth-action.ts +++ b/src/store/auth/auth-action.ts @@ -7,7 +7,7 @@ import { Dispatch } from "redux"; import { authService } from "../../services/services"; import { User } from "../../models/user"; -const actions = unionize({ +export const authActions = unionize({ SAVE_API_TOKEN: ofType(), LOGIN: {}, LOGOUT: {}, @@ -20,13 +20,11 @@ const actions = unionize({ }); export const getUserDetails = () => (dispatch: Dispatch): Promise => { - 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; -export default actions; +export type AuthAction = UnionOf; diff --git a/src/store/auth/auth-reducer.test.ts b/src/store/auth/auth-reducer.test.ts index 2e7c1a24..ea08e589 100644 --- a/src/store/auth/auth-reducer.test.ts +++ b/src/store/auth/auth-reducer.test.ts @@ -2,8 +2,8 @@ // // 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, @@ -23,7 +23,7 @@ describe('auth-reducer', () => { 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 @@ -40,7 +40,7 @@ describe('auth-reducer', () => { 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: { @@ -56,7 +56,7 @@ describe('auth-reducer', () => { 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 @@ -76,7 +76,7 @@ describe('auth-reducer', () => { 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: { @@ -94,7 +94,7 @@ describe('auth-reducer', () => { 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` ); @@ -103,7 +103,7 @@ describe('auth-reducer', () => { 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}` ); diff --git a/src/store/auth/auth-reducer.ts b/src/store/auth/auth-reducer.ts index f6974fd2..366385d5 100644 --- a/src/store/auth/auth-reducer.ts +++ b/src/store/auth/auth-reducer.ts @@ -2,7 +2,7 @@ // // 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"; @@ -12,8 +12,8 @@ export interface AuthState { 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); @@ -45,5 +45,3 @@ const authReducer = (state: AuthState = {}, action: AuthAction) => { default: () => state }); }; - -export default authReducer; diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts index 89d65244..8e5eb1e7 100644 --- a/src/store/context-menu/context-menu-actions.ts +++ b/src/store/context-menu/context-menu-actions.ts @@ -2,14 +2,15 @@ // // 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; -// export default actions; \ No newline at end of file +export type ContextMenuAction = UnionOf; diff --git a/src/store/context-menu/context-menu-reducer.ts b/src/store/context-menu/context-menu-reducer.ts index 9a825a5f..b20ad723 100644 --- a/src/store/context-menu/context-menu-reducer.ts +++ b/src/store/context-menu/context-menu-reducer.ts @@ -2,31 +2,32 @@ // // 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; diff --git a/src/store/data-explorer/data-explorer-action.ts b/src/store/data-explorer/data-explorer-action.ts index 3d7ba534..053f4194 100644 --- a/src/store/data-explorer/data-explorer-action.ts +++ b/src/store/data-explorer/data-explorer-action.ts @@ -6,7 +6,7 @@ import { default as unionize, ofType, UnionOf } from "unionize"; import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters"; import { DataColumns } from "../../components/data-table/data-table"; -const actions = unionize({ +export const dataExplorerActions = unionize({ RESET_PAGINATION: ofType<{ id: string }>(), REQUEST_ITEMS: ofType<{ id: string }>(), SET_COLUMNS: ofType<{ id: string, columns: DataColumns }>(), @@ -19,6 +19,4 @@ const actions = unionize({ SET_SEARCH_VALUE: ofType<{ id: string, searchValue: string }>(), }, { tag: "type", value: "payload" }); -export type DataExplorerAction = UnionOf; - -export default actions; +export type DataExplorerAction = UnionOf; diff --git a/src/store/data-explorer/data-explorer-reducer.test.tsx b/src/store/data-explorer/data-explorer-reducer.test.tsx index 0eb3c321..5b9f68ff 100644 --- a/src/store/data-explorer/data-explorer-reducer.test.tsx +++ b/src/store/data-explorer/data-explorer-reducer.test.tsx @@ -2,10 +2,11 @@ // // 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', () => { @@ -15,7 +16,7 @@ describe('data-explorer-reducer', () => { 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); }); @@ -24,15 +25,15 @@ describe('data-explorer-reducer', () => { 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"); }); @@ -49,25 +50,31 @@ describe('data-explorer-reducer', () => { 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); }); }); diff --git a/src/store/data-explorer/data-explorer-reducer.ts b/src/store/data-explorer/data-explorer-reducer.ts index 01126170..c112454b 100644 --- a/src/store/data-explorer/data-explorer-reducer.ts +++ b/src/store/data-explorer/data-explorer-reducer.ts @@ -3,7 +3,7 @@ // 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"; @@ -29,8 +29,8 @@ export const initialDataExplorer: DataExplorer = { export type DataExplorerState = Record; -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 })), @@ -61,8 +61,6 @@ const dataExplorerReducer = (state: DataExplorerState = {}, action: DataExplorer default: () => state }); -export default dataExplorerReducer; - export const getDataExplorer = (state: DataExplorerState, id: string) => state[id] || initialDataExplorer; diff --git a/src/store/details-panel/details-panel-action.ts b/src/store/details-panel/details-panel-action.ts index 630428cf..ba330f2e 100644 --- a/src/store/details-panel/details-panel-action.ts +++ b/src/store/details-panel/details-panel-action.ts @@ -3,28 +3,26 @@ // 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; +export type DetailsPanelAction = UnionOf; 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 })); }); }; diff --git a/src/store/details-panel/details-panel-reducer.ts b/src/store/details-panel/details-panel-reducer.ts index f57b9f1c..97de4a9a 100644 --- a/src/store/details-panel/details-panel-reducer.ts +++ b/src/store/details-panel/details-panel-reducer.ts @@ -2,7 +2,7 @@ // // 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 { @@ -15,12 +15,10 @@ const initialState = { 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; diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts index d7630d7a..3920b5a2 100644 --- a/src/store/navigation/navigation-action.ts +++ b/src/store/navigation/navigation-action.ts @@ -3,11 +3,11 @@ // 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"; diff --git a/src/store/project-panel/project-panel-middleware.ts b/src/store/project-panel/project-panel-middleware.ts index e72b6c1b..fbed1783 100644 --- a/src/store/project-panel/project-panel-middleware.ts +++ b/src/store/project-panel/project-panel-middleware.ts @@ -3,22 +3,21 @@ // 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 => { @@ -30,30 +29,30 @@ export const projectPanelMiddleware: Middleware = store => next => { } }; - 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; - 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) { @@ -62,7 +61,7 @@ export const projectPanelMiddleware: Middleware = store => next => { 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(), @@ -77,7 +76,7 @@ export const projectPanelMiddleware: Middleware = store => next => { .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, @@ -86,7 +85,7 @@ export const projectPanelMiddleware: Middleware = store => next => { })); }); } else { - store.dispatch(actions.SET_ITEMS({ + store.dispatch(dataExplorerActions.SET_ITEMS({ id: PROJECT_PANEL_ID, items: [], itemsAvailable: 0, diff --git a/src/store/project/project-action.ts b/src/store/project/project-action.ts index 3da60f65..075e77d1 100644 --- a/src/store/project/project-action.ts +++ b/src/store/project/project-action.ts @@ -6,10 +6,10 @@ import { default as unionize, ofType, UnionOf } from "unionize"; 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>(), @@ -26,13 +26,13 @@ const actions = unionize({ }); export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch) => { - dispatch(actions.PROJECTS_REQUEST(parentUuid)); + dispatch(projectActions.PROJECTS_REQUEST(parentUuid)); return projectService.list({ filters: FilterBuilder .create() .addEqual("ownerUuid", parentUuid) }).then(({ items: projects }) => { - dispatch(actions.PROJECTS_SUCCESS({ projects, parentItemId: parentUuid })); + dispatch(projectActions.PROJECTS_SUCCESS({ projects, parentItemId: parentUuid })); return projects; }); }; @@ -41,11 +41,10 @@ export const createProject = (project: Partial) => (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; -export default actions; +export type ProjectAction = UnionOf; diff --git a/src/store/project/project-reducer.test.ts b/src/store/project/project-reducer.test.ts index dbac4e92..c8eed87c 100644 --- a/src/store/project/project-reducer.test.ts +++ b/src/store/project/project-reducer.test.ts @@ -2,8 +2,8 @@ // // 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"; @@ -13,7 +13,7 @@ describe('project-reducer', () => { 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, @@ -64,7 +64,7 @@ describe('project-reducer', () => { 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); }); @@ -93,7 +93,7 @@ describe('project-reducer', () => { 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); }); @@ -124,7 +124,7 @@ describe('project-reducer', () => { 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); }); }); diff --git a/src/store/project/project-reducer.ts b/src/store/project/project-reducer.ts index a329e812..94a451a8 100644 --- a/src/store/project/project-reducer.ts +++ b/src/store/project/project-reducer.ts @@ -4,7 +4,7 @@ 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"; @@ -112,9 +112,9 @@ const initialState: ProjectState = { }; -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: "" }), @@ -174,5 +174,3 @@ const projectsReducer = (state: ProjectState = initialState, action: ProjectActi default: () => state }); }; - -export default projectsReducer; diff --git a/src/store/side-panel/side-panel-action.ts b/src/store/side-panel/side-panel-action.ts index 6a83946c..0dd6aad1 100644 --- a/src/store/side-panel/side-panel-action.ts +++ b/src/store/side-panel/side-panel-action.ts @@ -4,7 +4,7 @@ import { default as unionize, ofType, UnionOf } from "unionize"; -const actions = unionize({ +export const sidePanelActions = unionize({ TOGGLE_SIDE_PANEL_ITEM_OPEN: ofType(), TOGGLE_SIDE_PANEL_ITEM_ACTIVE: ofType(), RESET_SIDE_PANEL_ACTIVITY: ofType<{}>(), @@ -13,5 +13,4 @@ const actions = unionize({ value: 'payload' }); -export type SidePanelAction = UnionOf; -export default actions; \ No newline at end of file +export type SidePanelAction = UnionOf; diff --git a/src/store/side-panel/side-panel-reducer.test.ts b/src/store/side-panel/side-panel-reducer.test.ts index 942c16eb..e517fc85 100644 --- a/src/store/side-panel/side-panel-reducer.test.ts +++ b/src/store/side-panel/side-panel-reducer.test.ts @@ -2,8 +2,9 @@ // // 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', () => { @@ -12,7 +13,7 @@ describe('side-panel-reducer', () => { { id: "1", name: "Projects", - icon: "fas fa-th fa-fw", + icon: ProjectsIcon, open: false, active: false, } @@ -21,13 +22,13 @@ describe('side-panel-reducer', () => { { 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); }); @@ -36,7 +37,7 @@ describe('side-panel-reducer', () => { { id: "1", name: "Projects", - icon: "fas fa-th fa-fw", + icon: ProjectsIcon, open: false, active: false, } @@ -45,13 +46,13 @@ describe('side-panel-reducer', () => { { 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); }); @@ -60,7 +61,7 @@ describe('side-panel-reducer', () => { { id: "1", name: "Projects", - icon: "fas fa-th fa-fw", + icon: ProjectsIcon, open: false, active: true, } @@ -69,13 +70,13 @@ describe('side-panel-reducer', () => { { 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 +}); diff --git a/src/store/side-panel/side-panel-reducer.ts b/src/store/side-panel/side-panel-reducer.ts index ca26eeb6..2bbd6a11 100644 --- a/src/store/side-panel/side-panel-reducer.ts +++ b/src/store/side-panel/side-panel-reducer.ts @@ -3,17 +3,17 @@ // 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); @@ -48,7 +48,7 @@ export const sidePanelData = [ { id: SidePanelIdentifiers.Projects, name: "Projects", - icon: "fas fa-th fa-fw", + icon: ProjectsIcon, open: false, active: false, margin: true, @@ -57,31 +57,31 @@ export const sidePanelData = [ { 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, } ]; @@ -91,5 +91,3 @@ function resetSidePanelActivity(sidePanel: SidePanelItem[]) { t.active = false; } } - -export default sidePanelReducer; diff --git a/src/store/store.ts b/src/store/store.ts index 956fb460..01b06b95 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -7,12 +7,13 @@ import { routerMiddleware, routerReducer, RouterState } from "react-router-redux import thunkMiddleware from 'redux-thunk'; import { History } from "history"; -import projectsReducer, { ProjectState } from "./project/project-reducer"; -import sidePanelReducer, { SidePanelState } from './side-panel/side-panel-reducer'; -import authReducer, { AuthState } from "./auth/auth-reducer"; -import dataExplorerReducer, { DataExplorerState } from './data-explorer/data-explorer-reducer'; -import { 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 = @@ -27,6 +28,7 @@ export interface RootState { dataExplorer: DataExplorerState; sidePanel: SidePanelState; detailsPanel: DetailsPanelState; + contextMenu: ContextMenuState; } const rootReducer = combineReducers({ @@ -36,11 +38,12 @@ 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, diff --git a/src/utils/dialog-validator.tsx b/src/utils/dialog-validator.tsx new file mode 100644 index 00000000..9697a868 --- /dev/null +++ b/src/utils/dialog-validator.tsx @@ -0,0 +1,74 @@ +// 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 = theme => ({ + formInputError: { + color: "#ff0000", + marginLeft: "5px", + fontSize: "11px", + } +}); + +type ValidatorProps = { + value: string, + onChange: (isValid: boolean | string) => void; + render: (hasError: boolean) => React.ReactElement; + 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> { + 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 ( + + {this.props.render(!(isPatternValid && isLengthValid) && (isRequired || (!isRequired && value.length > 0)))} + {!isPatternValid && (isRequired || (!isRequired && value.length > 0)) ? + This field allow only alphanumeric characters, dashes, spaces and underscores.
: null} + {!isLengthValid ? + This field should have max 60 characters. : null} +
+ ); + } + } +); diff --git a/src/views-components/api-token/api-token.tsx b/src/views-components/api-token/api-token.tsx index e4ba4914..1d017ccd 100644 --- a/src/views-components/api-token/api-token.tsx +++ b/src/views-components/api-token/api-token.tsx @@ -5,33 +5,27 @@ 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, {}> { - 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, {}> { + 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(getUserDetails()).then(() => { + const rootUuid = authService.getRootUuid(); + this.props.dispatch(getProjectList(rootUuid)); + }); + } + render() { + return ; + } } - - 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(getUserDetails()).then(() => { - const rootUuid = authService.getRootUuid(); - this.props.dispatch(getProjectList(rootUuid)); - }); - } - render() { - return ; - } -} - -export default connect()(ApiToken); +); diff --git a/src/views-components/context-menu/action-sets/project-action-set.ts b/src/views-components/context-menu/action-sets/project-action-set.ts new file mode 100644 index 00000000..66dbd4d1 --- /dev/null +++ b/src/views-components/context-menu/action-sets/project-action-set.ts @@ -0,0 +1,19 @@ +// 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; } +}]]; diff --git a/src/views-components/context-menu/action-sets/root-project-action-set.ts b/src/views-components/context-menu/action-sets/root-project-action-set.ts new file mode 100644 index 00000000..139bd26f --- /dev/null +++ b/src/views-components/context-menu/action-sets/root-project-action-set.ts @@ -0,0 +1,15 @@ +// 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 })); + } +}]]; diff --git a/src/views-components/context-menu/context-menu-action-set.ts b/src/views-components/context-menu/context-menu-action-set.ts new file mode 100644 index 00000000..089580c2 --- /dev/null +++ b/src/views-components/context-menu/context-menu-action-set.ts @@ -0,0 +1,13 @@ +// 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>; diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx new file mode 100644 index 00000000..cc2fcb31 --- /dev/null +++ b/src/views-components/context-menu/context-menu.tsx @@ -0,0 +1,60 @@ +// 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 & { 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 & { 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(); + +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" +} diff --git a/src/views-components/create-project-dialog/create-project-dialog.tsx b/src/views-components/create-project-dialog/create-project-dialog.tsx index f75c4593..43621bf7 100644 --- a/src/views-components/create-project-dialog/create-project-dialog.tsx +++ b/src/views-components/create-project-dialog/create-project-dialog.tsx @@ -7,9 +7,9 @@ import { Dispatch } from "redux"; 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) => ({ @@ -27,7 +27,7 @@ export const addProject = (data: { name: string, description: string }) => const mapDispatchToProps = (dispatch: Dispatch) => ({ handleClose: () => { - dispatch(actions.CLOSE_PROJECT_CREATOR()); + dispatch(projectActions.CLOSE_PROJECT_CREATOR()); }, onSubmit: (data: { name: string, description: string }) => { return dispatch(addProject(data)) @@ -37,4 +37,4 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ } }); -export default connect(mapStateToProps, mapDispatchToProps)(DialogProjectCreate); +export const CreateProjectDialog = connect(mapStateToProps, mapDispatchToProps)(DialogProjectCreate); diff --git a/src/views-components/data-explorer/data-explorer.tsx b/src/views-components/data-explorer/data-explorer.tsx index b0e189f5..2645504c 100644 --- a/src/views-components/data-explorer/data-explorer.tsx +++ b/src/views-components/data-explorer/data-explorer.tsx @@ -4,10 +4,10 @@ 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"; @@ -24,35 +24,35 @@ const mapStateToProps = (state: RootState, { id }: Props) => 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) => { - dispatch(actions.TOGGLE_COLUMN({ id, columnName: column.name })); + dispatch(dataExplorerActions.TOGGLE_COLUMN({ id, columnName: column.name })); }, onSortToggle: (column: DataColumn) => { - dispatch(actions.TOGGLE_SORT({ id, columnName: column.name })); + dispatch(dataExplorerActions.TOGGLE_SORT({ id, columnName: column.name })); }, onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn) => { - 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); diff --git a/src/views-components/details-panel/collection-details.tsx b/src/views-components/details-panel/collection-details.tsx new file mode 100644 index 00000000..28817766 --- /dev/null +++ b/src/views-components/details-panel/collection-details.tsx @@ -0,0 +1,35 @@ +// 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 { + + getIcon(className?: string) { + return ; + } + + getDetails() { + return
+ + + + + + {/* Links but we dont have view */} + + + {/* Missing attrs */} + + +
; + } +} diff --git a/src/views-components/details-panel/details-data.tsx b/src/views-components/details-panel/details-data.tsx new file mode 100644 index 00000000..d20269cd --- /dev/null +++ b/src/views-components/details-panel/details-data.tsx @@ -0,0 +1,21 @@ +// 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 { + constructor(protected item: T) {} + + getTitle(): string { + return this.item.name; + } + + abstract getIcon(className?: string): React.ReactElement; + abstract getDetails(): React.ReactElement; + + getActivity(): React.ReactElement { + return
; + } +} diff --git a/src/views-components/details-panel/details-panel.tsx b/src/views-components/details-panel/details-panel.tsx index 34195784..ea8f2e40 100644 --- a/src/views-components/details-panel/details-panel.tsx +++ b/src/views-components/details-panel/details-panel.tsx @@ -3,89 +3,23 @@ // 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; - -class DetailsPanel extends React.Component { - state = { - tabsValue: 0 - }; - - handleChange = (event: any, value: boolean) => { - this.setState({ tabsValue: value }); - } - - renderTabContainer = (children: React.ReactElement) => - - {children} - - - render() { - const { classes, onCloseDrawer, isOpened, item } = this.props; - const { tabsValue } = this.state; - return ( - - - - - - - - - - {item.getTitle()} - - - - - - - - - - - - - - {tabsValue === 0 && this.renderTabContainer( - - {item.buildDetails()} - - )} - {tabsValue === 1 && this.renderTabContainer( - - )} - - - ); - } - -} +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'; @@ -118,31 +52,95 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ } }); - -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; + +export const DetailsPanel = withStyles(styles)( + connect(mapStateToProps, mapDispatchToProps)( + class extends React.Component { + state = { + tabsValue: 0 + }; + + handleChange = (event: any, value: boolean) => { + this.setState({ tabsValue: value }); + } + + renderTabContainer = (children: React.ReactElement) => + + {children} + + + render() { + const { classes, onCloseDrawer, isOpened, item } = this.props; + const { tabsValue } = this.state; + return ( + + + + + + {item.getIcon(classes.headerIcon)} + + + + {item.getTitle()} + + + + + {} + + + + + + + + + {tabsValue === 0 && this.renderTabContainer( + + {item.getDetails()} + + )} + {tabsValue === 1 && this.renderTabContainer( + + )} + + + ); + } + } + ) +); diff --git a/src/views-components/details-panel/empty-details.tsx b/src/views-components/details-panel/empty-details.tsx new file mode 100644 index 00000000..51112ce5 --- /dev/null +++ b/src/views-components/details-panel/empty-details.tsx @@ -0,0 +1,53 @@ +// 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 = (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; + +const EmptyState = withStyles(styles)( + ({ classes, details, message, children, icon: Icon }: EmptyStateProps) => + + + {message} + {details && {details}} + {children && {children}} + +); + +export class EmptyDetails extends DetailsData { + getIcon(className?: string) { + return ; + } + + getDetails() { + return ; + } +} diff --git a/src/views-components/details-panel/process-details.tsx b/src/views-components/details-panel/process-details.tsx new file mode 100644 index 00000000..931ff7ed --- /dev/null +++ b/src/views-components/details-panel/process-details.tsx @@ -0,0 +1,45 @@ +// 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 { + + getIcon(className?: string){ + return ; + } + + getDetails() { + return
+ + + + + {/* Missing attr */} + + + + {/* Missing attrs */} + + + + {/* Links but we dont have view */} + + + + + + + {/* Link but we dont have view */} + +
; + } +} diff --git a/src/views-components/details-panel/project-details.tsx b/src/views-components/details-panel/project-details.tsx new file mode 100644 index 00000000..84b37066 --- /dev/null +++ b/src/views-components/details-panel/project-details.tsx @@ -0,0 +1,33 @@ +// 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 { + + getIcon(className?: string) { + return ; + } + + getDetails() { + return
+ + {/* Missing attr */} + + + + + {/* Missing attr */} + + +
; + } +} diff --git a/src/views-components/dialog-create/dialog-project-create.tsx b/src/views-components/dialog-create/dialog-project-create.tsx index 6fb8a699..34c655e2 100644 --- a/src/views-components/dialog-create/dialog-project-create.tsx +++ b/src/views-components/dialog-create/dialog-project-create.tsx @@ -125,4 +125,4 @@ const styles: StyleRulesCallback = theme => ({ export default compose( reduxForm({ form: 'projectCreateDialog' }), withStyles(styles) -)(DialogProjectCreate); \ No newline at end of file +)(DialogProjectCreate); diff --git a/src/views-components/main-app-bar/main-app-bar.test.tsx b/src/views-components/main-app-bar/main-app-bar.test.tsx index f58b26a0..6d5c9de8 100644 --- a/src/views-components/main-app-bar/main-app-bar.test.tsx +++ b/src/views-components/main-app-bar/main-app-bar.test.tsx @@ -5,10 +5,10 @@ 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"; @@ -28,6 +28,7 @@ describe("", () => { const mainAppBar = mount( ", () => { ", () => { const mainAppBar = mount( ", () => { const mainAppBar = mount( = (props) => { onClick={props.onBreadcrumbClick} onContextMenu={props.onContextMenu} /> } - - - + { props.user && + + + } ; }; - const renderMenuForUser = ({ user, menuItems, onMenuItemClick }: MainAppBarProps) => { return ( <> - + - + } id="account-menu"> {getUserFullname(user)} {renderMenuItems(menuItems.accountMenu, onMenuItemClick)} - + } id="help-menu"> {renderMenuItems(menuItems.helpMenu, onMenuItemClick)} @@ -120,5 +117,3 @@ const renderMenuItems = (menuItems: MainAppBarMenuItem[], onMenuItemClick: (menu )); }; - -export default MainAppBar; diff --git a/src/views-components/project-list/project-list.tsx b/src/views-components/project-list/project-list.tsx deleted file mode 100644 index 88cd0f7c..00000000 --- a/src/views-components/project-list/project-list.tsx +++ /dev/null @@ -1,61 +0,0 @@ -// 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 = (theme: Theme) => ({ - root: { - width: '100%', - marginTop: theme.spacing.unit * 3, - overflowX: 'auto', - }, - table: { - minWidth: 700, - }, -}); - -interface ProjectListProps { -} - -class ProjectList extends React.Component, {}> { - render() { - const {classes} = this.props; - return - - - - Name - Status - Type - Shared by - File size - Last modified - - - - - Project 1 - Complete - Project - John Doe - 1.5 GB - 9:22 PM - - -
-
; - } -} - -export default withStyles(styles)(ProjectList); diff --git a/src/views-components/project-tree/project-tree.test.tsx b/src/views-components/project-tree/project-tree.test.tsx index d53f8a9e..56566da6 100644 --- a/src/views-components/project-tree/project-tree.test.tsx +++ b/src/views-components/project-tree/project-tree.test.tsx @@ -10,7 +10,7 @@ import ListItemIcon from '@material-ui/core/ListItemIcon'; 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'; @@ -81,9 +81,9 @@ describe("ProjectTree component", () => { ] } ]; - const wrapper = mount(); @@ -98,9 +98,9 @@ describe("ProjectTree component", () => { active: true, status: 1 }; - const wrapper = mount(); diff --git a/src/views-components/project-tree/project-tree.tsx b/src/views-components/project-tree/project-tree.tsx index 17592a7f..c9d4c3e3 100644 --- a/src/views-components/project-tree/project-tree.tsx +++ b/src/views-components/project-tree/project-tree.tsx @@ -5,12 +5,19 @@ 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 = (theme: ArvadosTheme) => ({ + root: { + marginLeft: `${theme.spacing.unit * 1.5}px`, + } +}); export interface ProjectTreeProps { projects: Array>; @@ -19,50 +26,26 @@ export interface ProjectTreeProps { onContextMenu: (event: React.MouseEvent, item: TreeItem) => void; } -class ProjectTree extends React.Component> { - render(): ReactElement { - const { classes, projects, toggleOpen, toggleActive, onContextMenu } = this.props; - const { active, listItemText, row, treeContainer } = classes; - return ( -
- ) => - - - - - {project.data.name} - } /> - - } /> -
- ); +export const ProjectTree = withStyles(styles)( + class ProjectTreeGeneric extends React.Component> { + render(): ReactElement { + const { classes, projects, toggleOpen, toggleActive, onContextMenu } = this.props; + return ( +
+ ) => + + }/> +
+ ); + } } -} - -type CssRules = 'active' | 'listItemText' | 'row' | 'treeContainer'; - -const styles: StyleRulesCallback = (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); +); diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx index e34ea1ec..daf22b11 100644 --- a/src/views/project-panel/project-panel.tsx +++ b/src/views/project-panel/project-panel.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; 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'; @@ -16,58 +16,12 @@ import { ContainerRequestState } from '../../models/container-request'; 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, item: ProjectPanelItem) => void; - onDialogOpen: (ownerUuid: string) => void; - onItemDoubleClick: (item: ProjectPanelItem) => void, - onItemRouteChange: (itemId: string) => void -} - & DispatchProp - & WithStyles - & RouteComponentProps<{ id: string }>; - -class ProjectPanel extends React.Component { - render() { - return
-
- - - -
- item.uuid} /> -
; - } - - 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 = theme => ({ +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ toolbar: { paddingBottom: theme.spacing.unit * 3, textAlign: "right" @@ -78,11 +32,7 @@ const styles: StyleRulesCallback = theme => ({ }); const renderName = (item: ProjectPanelItem) => - + {renderIcon(item)} @@ -97,20 +47,19 @@ const renderName = (item: ProjectPanelItem) => const renderIcon = (item: ProjectPanelItem) => { switch (item.kind) { case ResourceKind.Project: - return ; + return ; case ResourceKind.Collection: - return ; + return ; case ResourceKind.Process: - return ; + return ; default: - return ; + return ; } }; -const renderDate = (date: string) => - - {formatDate(date)} - ; +const renderDate = (date: string) => { + return {formatDate(date)}; +}; const renderFileSize = (fileSize?: number) => @@ -118,92 +67,159 @@ const renderFileSize = (fileSize?: number) => ; const renderOwner = (owner: string) => - + {owner} ; -const renderType = (type: string) => { - return +const renderType = (type: string) => + {resourceLabel(type)} ; -}; const renderStatus = (item: ProjectPanelItem) => - + {item.status || "-"} ; 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 = [{ - name: ProjectPanelColumnNames.Name, - selected: true, - sortDirection: SortDirection.Asc, - render: renderName, - width: "450px" -}, { - name: "Status", - selected: true, - filters: [{ - name: ContainerRequestState.Committed, +export const columns: DataColumns = [ + { + 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, item: ProjectPanelItem) => void; + onDialogOpen: (ownerUuid: string) => void; + onItemDoubleClick: (item: ProjectPanelItem) => void; + onItemRouteChange: (itemId: string) => void; +} + +type ProjectPanelProps = ProjectPanelDataProps & ProjectPanelActionProps & DispatchProp + & WithStyles & RouteComponentProps<{ id: string }>; + +export const ProjectPanel = withStyles(styles)( connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))( - ProjectPanel)); + class extends React.Component { + render() { + const { classes } = this.props; + return
+
+ + + +
+ item.uuid} /> +
; + } + + 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 diff --git a/src/views/workbench/workbench.test.tsx b/src/views/workbench/workbench.test.tsx index 79a98ad6..538b8e78 100644 --- a/src/views/workbench/workbench.test.tsx +++ b/src/views/workbench/workbench.test.tsx @@ -4,9 +4,9 @@ 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'; diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index b2bdac80..b1e7cd78 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -7,34 +7,31 @@ import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/st 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; @@ -98,10 +95,6 @@ interface NavMenuItem extends MainAppBarMenuItem { } interface WorkbenchState { - contextMenu: { - anchorEl?: HTMLElement; - itemUuid?: string; - }; anchorEl: any; searchText: string; menuItems: { @@ -111,207 +104,157 @@ interface WorkbenchState { }; } - -class Workbench extends React.Component { - 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( + (state: RootState) => ({ + projects: state.projects.items, + currentProjectId: state.projects.currentItemId, + user: state.auth.user, + sidePanelItems: state.sidePanel + }) + )( + class extends React.Component { + 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(setProjectItem(itemId, ItemMode.BOTH)); - this.props.dispatch(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, 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 ( +
+
+ +
+ {user && + +
+ this.openContextMenu(event, authService.getUuid() || "", ContextMenuKind.RootProject)}> + this.props.dispatch(setProjectItem(itemId, ItemMode.OPEN))} + onContextMenu={(event, item) => this.openContextMenu(event, item.data.uuid, ContextMenuKind.Project)} + toggleActive={itemId => { + this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE)); + this.props.dispatch(loadDetails(itemId, ResourceKind.Project)); + this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.Projects)); + }} /> + + } +
+
+ + + +
+ {user && } +
+ + +
+ ); + } - 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 }>) => this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE))} + onContextMenu={(event, item) => this.openContextMenu(event, item.uuid, ContextMenuKind.Project)} + onDialogOpen={this.handleCreationDialogOpen} + onItemClick={item => { + this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind)); + }} + onItemDoubleClick={item => { + this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE)); + this.props.dispatch(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(setProjectItem(itemId, ItemMode.BOTH)); + this.props.dispatch(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, 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, 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, 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 ( -
-
- -
- {user && - -
- this.openContextMenu(event, authService.getUuid() || "")}> - this.props.dispatch(setProjectItem(itemId, ItemMode.OPEN))} - onContextMenu={(event, item) => this.openContextMenu(event, item.data.uuid)} - toggleActive={itemId => { - this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE)); - this.props.dispatch(loadDetails(itemId, ResourceKind.Project)); - this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.Projects)); - }} /> - - } -
-
- - - -
- -
- - -
- ); - } - - renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE))} - onContextMenu={(event, item) => this.openContextMenu(event, item.uuid)} - onDialogOpen={this.handleCreationDialogOpen} - onItemClick={item => { - this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind)); - }} - onItemDoubleClick={item => { - this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE)); - this.props.dispatch(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( - (state: RootState) => ({ - projects: state.projects.items, - currentProjectId: state.projects.currentItemId, - user: state.auth.user, - sidePanelItems: state.sidePanel - }) -)( - withStyles(styles)(Workbench) + ) ); diff --git a/tslint.json b/tslint.json index 0ddfc627..85b43690 100644 --- a/tslint.json +++ b/tslint.json @@ -19,7 +19,8 @@ "linterOptions": { "exclude": [ "config/**/*.js", - "node_modules/**/*.ts" + "node_modules/**/*.ts", + "coverage/lcov-report/*.js" ] } } diff --git a/yarn.lock b/yarn.lock index 0034169d..3557ebef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,29 +3,29 @@ "@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" @@ -37,6 +37,7 @@ 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" @@ -46,12 +47,12 @@ 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" @@ -62,12 +63,12 @@ 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" @@ -75,9 +76,9 @@ 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" "*" @@ -86,9 +87,9 @@ 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" @@ -97,13 +98,13 @@ 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" @@ -112,9 +113,9 @@ "@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" @@ -136,9 +137,9 @@ "@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" "*" @@ -149,9 +150,9 @@ 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" @@ -226,8 +227,8 @@ ajv@^5.0.0, ajv@^5.1.0, ajv@^5.1.5: 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" @@ -659,8 +660,8 @@ babel-jest@^22.1.0, babel-jest@^22.4.4: 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" @@ -1040,8 +1041,8 @@ babel-preset-jest@^22.0.1, babel-preset-jest@^22.4.4: 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" @@ -1153,8 +1154,8 @@ batch@0.6.1: 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" @@ -1259,8 +1260,8 @@ browser-process-hrtime@^0.1.2: 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" @@ -1284,12 +1285,13 @@ browserify-cipher@^1.0.0: 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" @@ -1444,12 +1446,12 @@ caniuse-api@^1.5.2: 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" @@ -1529,8 +1531,8 @@ chokidar@^1.6.0, chokidar@^1.7.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" @@ -1539,12 +1541,13 @@ chokidar@^2.0.2: 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" @@ -1699,9 +1702,9 @@ combined-stream@1.0.6, combined-stream@~1.0.5: 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" @@ -1719,22 +1722,22 @@ component-emitter@^1.2.1: 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: @@ -2015,8 +2018,8 @@ csso@~2.3.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" @@ -2024,11 +2027,7 @@ cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": 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" @@ -2369,8 +2368,8 @@ ee-first@1.1.1: 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" @@ -2430,11 +2429,10 @@ enzyme-adapter-react-16@^1.1.1: 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: @@ -2465,8 +2463,8 @@ errno@^0.1.3, errno@~0.1.7: 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" @@ -2558,8 +2556,8 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^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" @@ -2586,8 +2584,8 @@ esprima@^3.1.3: 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" @@ -2636,10 +2634,10 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: 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" @@ -2939,8 +2937,8 @@ flush-write-stream@^1.0.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" @@ -3049,7 +3047,7 @@ fs.realpath@^1.0.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: @@ -3082,8 +3080,8 @@ gauge@~2.7.3: 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" @@ -3318,11 +3316,11 @@ hash-base@^3.0.0: 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" @@ -3347,8 +3345,8 @@ hmac-drbg@^1.0.0: 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" @@ -3368,8 +3366,8 @@ homedir-polyfill@^1.0.1: 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" @@ -3395,16 +3393,16 @@ html-entities@^1.2.0: 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" @@ -3679,8 +3677,8 @@ is-builtin-module@^1.0.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" @@ -3833,12 +3831,6 @@ is-obj@^1.0.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" @@ -4304,13 +4296,17 @@ jest@22.4.2: 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" @@ -4501,8 +4497,8 @@ jss-vendor-prefixer@^7.0.0: 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" @@ -4632,6 +4628,10 @@ lodash.camelcase@^4.3.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" @@ -4694,10 +4694,10 @@ longest@^1.0.1: 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" @@ -4804,7 +4804,7 @@ merge-stream@^1.0.1: 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" @@ -4855,19 +4855,15 @@ miller-rabin@^4.0.0: 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" @@ -4887,7 +4883,7 @@ min-document@^2.19.0: 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" @@ -4995,15 +4991,14 @@ nan@^2.9.2: 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" @@ -5024,7 +5019,7 @@ nearley@^2.7.10: 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: @@ -5103,16 +5098,16 @@ node-notifier@^5.2.1: 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" @@ -5204,8 +5199,8 @@ number-is-nan@^1.0.0: 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" @@ -5232,8 +5227,8 @@ object-is@^1.0.1: 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" @@ -5581,7 +5576,7 @@ pn@^1.1.0: 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" @@ -5873,8 +5868,8 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0 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" @@ -5942,11 +5937,10 @@ promise@^7.1.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" @@ -6094,7 +6088,7 @@ raw-body@2.3.2: 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: @@ -6152,8 +6146,8 @@ react-is@^16.4.1: 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" @@ -6165,13 +6159,6 @@ react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4: 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" @@ -6277,12 +6264,13 @@ react-test-renderer@^16.0.0-0: 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" @@ -6354,12 +6342,12 @@ readdirp@^2.0.0: 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: @@ -6398,8 +6386,8 @@ reduce-function-call@^1.0.1: 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" @@ -6449,10 +6437,14 @@ regenerate@^1.2.1: version "1.4.0" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" -regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.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" @@ -6636,8 +6628,8 @@ resolve@1.6.0: 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" @@ -6708,7 +6700,7 @@ safe-buffer@5.1.1: 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" @@ -7418,8 +7410,8 @@ toposort@^1.0.0: 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" @@ -7476,8 +7468,8 @@ tsconfig-paths-webpack-plugin@^2.0.0: 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" @@ -7485,8 +7477,8 @@ tsconfig-paths@^3.1.1: 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" @@ -7499,8 +7491,8 @@ tslint-react@^3.2.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" @@ -7513,11 +7505,11 @@ tslint@^5.7.0: 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" @@ -7567,11 +7559,11 @@ uglify-es@^3.3.4: 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: @@ -7583,13 +7575,6 @@ 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" @@ -7603,8 +7588,8 @@ uglifyjs-webpack-plugin@^0.4.6: 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" @@ -7659,8 +7644,8 @@ unique-string@^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" @@ -7677,7 +7662,7 @@ unzip-response@^2.0.1: 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" @@ -7743,10 +7728,8 @@ url@^0.11.0: 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" @@ -7788,8 +7771,8 @@ uuid@^2.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"