// SPDX-License-Identifier: AGPL-3.0
import { CommonResourceService } from "./common-resource-service";
-import axios from "axios";
+import axios, { AxiosInstance } from "axios";
import MockAdapter from "axios-mock-adapter";
+import { Resource } from "../../models/resource";
+
+export const mockResourceService = <R extends Resource, C extends CommonResourceService<R>>(Service: new (client: AxiosInstance) => C) => {
+ const axiosInstance = axios.create();
+ const axiosMock = new MockAdapter(axiosInstance);
+ const service = new Service(axiosInstance);
+ Object.keys(service).map(key => service[key] = jest.fn());
+ return service;
+};
describe("CommonResourceService", () => {
const axiosInstance = axios.create();
const onItemClick = jest.fn();
const contextMenu = mount(<ContextMenu
anchorEl={document.createElement("div")}
+ open={true}
onClose={jest.fn()}
onItemClick={onItemClick}
items={items} />);
import { IconType } from "../icon/icon";
export interface ContextMenuItem {
- name: string;
- icon: IconType;
+ name?: string | React.ComponentType;
+ icon?: IconType;
+ component?: React.ComponentType<any>;
}
export type ContextMenuItemGroup = ContextMenuItem[];
export interface ContextMenuProps {
anchorEl?: HTMLElement;
items: ContextMenuItemGroup[];
+ open: boolean;
onItemClick: (action: ContextMenuItem) => void;
onClose: () => void;
}
export class ContextMenu extends React.PureComponent<ContextMenuProps> {
render() {
- const { anchorEl, items, onClose, onItemClick} = this.props;
+ const { anchorEl, items, open, onClose, onItemClick } = this.props;
return <Popover
anchorEl={anchorEl}
- open={!!anchorEl}
+ open={open}
onClose={onClose}
transformOrigin={DefaultTransformOrigin}
anchorOrigin={DefaultTransformOrigin}
button
key={actionIndex}
onClick={() => onItemClick(item)}>
- <ListItemIcon>
- <item.icon/>
- </ListItemIcon>
- <ListItemText>
- {item.name}
- </ListItemText>
+ {item.icon &&
+ <ListItemIcon>
+ <item.icon />
+ </ListItemIcon>}
+ {item.name &&
+ <ListItemText>
+ {item.name}
+ </ListItemText>}
+ {item.component &&
+ <item.component />}
</ListItem>)}
{groupIndex < items.length - 1 && <Divider />}
</React.Fragment>)}
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";
+import { resourceActionSet } from './views-components/context-menu/action-sets/resource-action-set';
addMenuActionSet(ContextMenuKind.RootProject, rootProjectActionSet);
addMenuActionSet(ContextMenuKind.Project, projectActionSet);
+addMenuActionSet(ContextMenuKind.Resource, resourceActionSet);
fetchConfig()
.then(config => {
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Resource } from "./resource";
+
+export interface LinkResource extends Resource {
+ headUuid: string;
+ tailUuid: string;
+ linkClass: string;
+ name: string;
+ properties: {};
+}
+
+export enum LinkClass {
+ STAR = 'star'
+}
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { LinkService } from "../link-service/link-service";
+import { GroupsService, GroupContentsResource } from "../groups-service/groups-service";
+import { FavoriteService } from "./favorite-service";
+import { LinkClass, LinkResource } from "../../models/link";
+import { mockResourceService } from "../../common/api/common-resource-service.test";
+import { FilterBuilder } from "../../common/api/filter-builder";
+
+describe("FavoriteService", () => {
+
+ let linkService: LinkService;
+ let groupService: GroupsService;
+
+ beforeEach(() => {
+ linkService = mockResourceService(LinkService);
+ groupService = mockResourceService(GroupsService);
+ });
+
+ it("marks resource as favorite", async () => {
+ linkService.create = jest.fn().mockReturnValue(Promise.resolve({ uuid: "newUuid" }));
+ const favoriteService = new FavoriteService(linkService, groupService);
+
+ const newFavorite = await favoriteService.create({ userUuid: "userUuid", resource: { uuid: "resourceUuid", name: "resource" } });
+
+ expect(linkService.create).toHaveBeenCalledWith({
+ ownerUuid: "userUuid",
+ tailUuid: "userUuid",
+ headUuid: "resourceUuid",
+ linkClass: LinkClass.STAR,
+ name: "resource"
+ });
+ expect(newFavorite.uuid).toEqual("newUuid");
+
+ });
+
+ it("unmarks resource as favorite", async () => {
+ const list = jest.fn().mockReturnValue(Promise.resolve({ items: [{ uuid: "linkUuid" }] }));
+ const filters = FilterBuilder
+ .create<LinkResource>()
+ .addEqual('tailUuid', "userUuid")
+ .addEqual('headUuid', "resourceUuid")
+ .addEqual('linkClass', LinkClass.STAR);
+ linkService.list = list;
+ linkService.delete = jest.fn().mockReturnValue(Promise.resolve({ uuid: "linkUuid" }));
+ const favoriteService = new FavoriteService(linkService, groupService);
+
+ const newFavorite = await favoriteService.delete({ userUuid: "userUuid", resourceUuid: "resourceUuid" });
+
+ expect(list.mock.calls[0][0].filters.getFilters()).toEqual(filters.getFilters());
+ expect(linkService.delete).toHaveBeenCalledWith("linkUuid");
+ expect(newFavorite[0].uuid).toEqual("linkUuid");
+ });
+
+ it("lists favorite resources", async () => {
+ const list = jest.fn().mockReturnValue(Promise.resolve({ items: [{ headUuid: "headUuid" }] }));
+ const listFilters = FilterBuilder
+ .create<LinkResource>()
+ .addEqual('tailUuid', "userUuid")
+ .addEqual('linkClass', LinkClass.STAR);
+ const contents = jest.fn().mockReturnValue(Promise.resolve({ items: [{ uuid: "resourceUuid" }] }));
+ const contentFilters = FilterBuilder.create<GroupContentsResource>().addIn('uuid', ["headUuid"]);
+ linkService.list = list;
+ groupService.contents = contents;
+ const favoriteService = new FavoriteService(linkService, groupService);
+
+ const favorites = await favoriteService.list("userUuid");
+
+ expect(list.mock.calls[0][0].filters.getFilters()).toEqual(listFilters.getFilters());
+ expect(contents.mock.calls[0][0]).toEqual("userUuid");
+ expect(contents.mock.calls[0][1].filters.getFilters()).toEqual(contentFilters.getFilters());
+ expect(favorites).toEqual({ items: [{ uuid: "resourceUuid" }] });
+ });
+
+ it("checks if resources are present in favorites", async () => {
+ const list = jest.fn().mockReturnValue(Promise.resolve({ items: [{ headUuid: "foo" }] }));
+ const listFilters = FilterBuilder
+ .create<LinkResource>()
+ .addIn("headUuid", ["foo", "oof"])
+ .addEqual("tailUuid", "userUuid")
+ .addEqual("linkClass", LinkClass.STAR);
+ linkService.list = list;
+ const favoriteService = new FavoriteService(linkService, groupService);
+
+ const favorites = await favoriteService.checkPresenceInFavorites("userUuid", ["foo", "oof"]);
+
+ expect(list.mock.calls[0][0].filters.getFilters()).toEqual(listFilters.getFilters());
+ expect(favorites).toEqual({ foo: true, oof: false });
+ });
+
+});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { LinkService } from "../link-service/link-service";
+import { GroupsService, GroupContentsResource } from "../groups-service/groups-service";
+import { LinkResource, LinkClass } from "../../models/link";
+import { FilterBuilder } from "../../common/api/filter-builder";
+import { ListArguments, ListResults } from "../../common/api/common-resource-service";
+import { OrderBuilder } from "../../common/api/order-builder";
+
+export interface FavoriteListArguments extends ListArguments {
+ filters?: FilterBuilder<LinkResource>;
+ order?: OrderBuilder<LinkResource>;
+}
+export class FavoriteService {
+ constructor(
+ private linkService: LinkService,
+ private groupsService: GroupsService
+ ) { }
+
+ create(data: { userUuid: string; resource: { uuid: string; name: string } }) {
+ return this.linkService.create({
+ ownerUuid: data.userUuid,
+ tailUuid: data.userUuid,
+ headUuid: data.resource.uuid,
+ linkClass: LinkClass.STAR,
+ name: data.resource.name
+ });
+ }
+
+ delete(data: { userUuid: string; resourceUuid: string; }) {
+ return this.linkService
+ .list({
+ filters: FilterBuilder
+ .create<LinkResource>()
+ .addEqual('tailUuid', data.userUuid)
+ .addEqual('headUuid', data.resourceUuid)
+ .addEqual('linkClass', LinkClass.STAR)
+ })
+ .then(results => Promise.all(
+ results.items.map(item => this.linkService.delete(item.uuid))));
+ }
+
+ list(userUuid: string, args: FavoriteListArguments = {}): Promise<ListResults<GroupContentsResource>> {
+ const listFilter = FilterBuilder
+ .create<LinkResource>()
+ .addEqual('tailUuid', userUuid)
+ .addEqual('linkClass', LinkClass.STAR);
+
+ return this.linkService
+ .list({
+ ...args,
+ filters: args.filters ? args.filters.concat(listFilter) : listFilter
+ })
+ .then(results => {
+ const uuids = results.items.map(item => item.headUuid);
+ return this.groupsService.contents(userUuid, {
+ limit: args.limit,
+ offset: args.offset,
+ filters: FilterBuilder.create<GroupContentsResource>().addIn('uuid', uuids),
+ recursive: true
+ });
+ });
+ }
+
+ checkPresenceInFavorites(userUuid: string, resourceUuids: string[]): Promise<Record<string, boolean>> {
+ return this.linkService
+ .list({
+ filters: FilterBuilder
+ .create<LinkResource>()
+ .addIn("headUuid", resourceUuids)
+ .addEqual("tailUuid", userUuid)
+ .addEqual("linkClass", LinkClass.STAR)
+ })
+ .then(({ items }) => resourceUuids.reduce((results, uuid) => {
+ const isFavorite = items.some(item => item.headUuid === uuid);
+ return { ...results, [uuid]: isFavorite };
+ }, {}));
+ }
+
+
+}
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { CommonResourceService } from "../../common/api/common-resource-service";
+import { LinkResource } from "../../models/link";
+import { AxiosInstance } from "axios";
+
+export class LinkService extends CommonResourceService<LinkResource> {
+ constructor(serverApi: AxiosInstance) {
+ super(serverApi, "links");
+ }
+}
\ No newline at end of file
import { GroupsService } from "./groups-service/groups-service";
import { serverApi } from "../common/api/server-api";
import { ProjectService } from "./project-service/project-service";
+import { LinkService } from "./link-service/link-service";
+import { FavoriteService } from "./favorite-service/favorite-service";
export const authService = new AuthService(serverApi);
export const groupsService = new GroupsService(serverApi);
export const projectService = new ProjectService(serverApi);
+export const linkService = new LinkService(serverApi);
+export const favoriteService = new FavoriteService(linkService, groupsService);
import { contextMenuActions, ContextMenuAction } from "./context-menu-actions";
export interface ContextMenuState {
+ open: boolean;
position: ContextMenuPosition;
resource?: ContextMenuResource;
}
export interface ContextMenuResource {
uuid: string;
kind: string;
+ name: string;
}
const initialState = {
+ open: false,
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 })
+ OPEN_CONTEXT_MENU: ({ resource, position }) => ({ open: true, resource, position }),
+ CLOSE_CONTEXT_MENU: () => ({ ...state, open: false })
});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { unionize, ofType, UnionOf } from "unionize";
+import { Dispatch } from "redux";
+import { favoriteService } from "../../services/services";
+import { RootState } from "../store";
+import { checkFavorite } from "./favorites-reducer";
+
+export const favoritesActions = unionize({
+ TOGGLE_FAVORITE: ofType<{ resourceUuid: string }>(),
+ CHECK_PRESENCE_IN_FAVORITES: ofType<string[]>(),
+ UPDATE_FAVORITES: ofType<Record<string, boolean>>()
+}, { tag: 'type', value: 'payload' });
+
+export type FavoritesAction = UnionOf<typeof favoritesActions>;
+
+export const toggleFavorite = (resource: { uuid: string; name: string }) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const userUuid = getState().auth.user!.uuid;
+ dispatch(favoritesActions.TOGGLE_FAVORITE({ resourceUuid: resource.uuid }));
+ const isFavorite = checkFavorite(resource.uuid, getState().favorites);
+ const promise: (any) = isFavorite
+ ? favoriteService.delete({ userUuid, resourceUuid: resource.uuid })
+ : favoriteService.create({ userUuid, resource });
+
+ promise
+ .then(() => {
+ dispatch(favoritesActions.UPDATE_FAVORITES({ [resource.uuid]: !isFavorite }));
+ });
+ };
+
+export const checkPresenceInFavorites = (resourceUuids: string[]) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const userUuid = getState().auth.user!.uuid;
+ dispatch(favoritesActions.CHECK_PRESENCE_IN_FAVORITES(resourceUuids));
+ favoriteService
+ .checkPresenceInFavorites(userUuid, resourceUuids)
+ .then(results => {
+ dispatch(favoritesActions.UPDATE_FAVORITES(results));
+ });
+ };
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { FavoritesAction, favoritesActions } from "./favorites-actions";
+
+export type FavoritesState = Record<string, boolean>;
+
+export const favoritesReducer = (state: FavoritesState = {}, action: FavoritesAction) =>
+ favoritesActions.match(action, {
+ UPDATE_FAVORITES: favorites => ({...state, ...favorites}),
+ default: () => state
+ });
+
+export const checkFavorite = (uuid: string, state: FavoritesState) => state[uuid] === true;
\ No newline at end of file
import { OrderBuilder } from "../../common/api/order-builder";
import { GroupContentsResource, GroupContentsResourcePrefix } from "../../services/groups-service/groups-service";
import { SortDirection } from "../../components/data-table/data-column";
+import { checkPresenceInFavorites } from "../favorites/favorites-actions";
export const projectPanelMiddleware: Middleware = store => next => {
next(dataExplorerActions.SET_COLUMNS({ id: PROJECT_PANEL_ID, columns }));
page: Math.floor(response.offset / response.limit),
rowsPerPage: response.limit
}));
+ store.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
});
} else {
store.dispatch(dataExplorerActions.SET_ITEMS({
import { Dispatch } from "redux";
import { FilterBuilder } from "../../common/api/filter-builder";
import { RootState } from "../store";
+import { checkPresenceInFavorites } from "../favorites/favorites-actions";
export const projectActions = unionize({
OPEN_PROJECT_CREATOR: ofType<{ ownerUuid: string }>(),
value: 'payload'
});
-export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch) => {
+export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch, getState: () => RootState) => {
dispatch(projectActions.PROJECTS_REQUEST(parentUuid));
return projectService.list({
filters: FilterBuilder
.addEqual("ownerUuid", parentUuid)
}).then(({ items: projects }) => {
dispatch(projectActions.PROJECTS_SUCCESS({ projects, parentItemId: parentUuid }));
+ dispatch<any>(checkPresenceInFavorites(projects.map(project => project.uuid)));
return projects;
});
};
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';
+import { FavoritesState, favoritesReducer } from './favorites/favorites-reducer';
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
sidePanel: SidePanelState;
detailsPanel: DetailsPanelState;
contextMenu: ContextMenuState;
+ favorites: FavoritesState;
}
const rootReducer = combineReducers({
sidePanel: sidePanelReducer,
detailsPanel: detailsPanelReducer,
contextMenu: contextMenuReducer,
- form: formReducer
+ form: formReducer,
+ favorites: favoritesReducer,
});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { ListItemIcon, ListItemText } from "@material-ui/core";
+import { FavoriteIcon, AddFavoriteIcon, RemoveFavoriteIcon } from "../../../components/icon/icon";
+import { connect } from "react-redux";
+import { RootState } from "../../../store/store";
+
+const mapStateToProps = (state: RootState) => ({
+ isFavorite: state.contextMenu.resource && state.favorites[state.contextMenu.resource.uuid] === true
+});
+
+export const ToggleFavoriteAction = connect(mapStateToProps)((props: { isFavorite: boolean }) =>
+ <>
+ <ListItemIcon>
+ {props.isFavorite
+ ? <RemoveFavoriteIcon />
+ : <AddFavoriteIcon />}
+ </ListItemIcon>
+ <ListItemText>
+ {props.isFavorite
+ ? <>Remove from favorites</>
+ : <>Add to favorites</>}
+ </ListItemText>
+ </>);
import { ContextMenuActionSet } from "../context-menu-action-set";
import { projectActions } from "../../../store/project/project-action";
-import { ShareIcon, NewProjectIcon } from "../../../components/icon/icon";
+import { NewProjectIcon } from "../../../components/icon/icon";
+import { ToggleFavoriteAction } from "./favorite-action";
+import { toggleFavorite } from "../../../store/favorites/favorites-actions";
export const projectActionSet: ContextMenuActionSet = [[{
icon: NewProjectIcon,
dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid }));
}
}, {
- icon: ShareIcon,
- name: "Share",
- execute: () => { return; }
+ component: ToggleFavoriteAction,
+ execute: (dispatch, resource) => {
+ dispatch<any>(toggleFavorite(resource));
+ }
}]];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import { ToggleFavoriteAction } from "./favorite-action";
+import { toggleFavorite } from "../../../store/favorites/favorites-actions";
+
+export const resourceActionSet: ContextMenuActionSet = [[{
+ component: ToggleFavoriteAction,
+ execute: (dispatch, resource) => {
+ dispatch<any>(toggleFavorite(resource));
+ }
+}]];
import { ContextMenuActionSet, ContextMenuAction } from "./context-menu-action-set";
import { Dispatch } from "redux";
-type DataProps = Pick<ContextMenuProps, "anchorEl" | "items"> & { resource?: ContextMenuResource };
+type DataProps = Pick<ContextMenuProps, "anchorEl" | "items" | "open"> & { resource?: ContextMenuResource };
const mapStateToProps = (state: RootState): DataProps => {
- const { position, resource } = state.contextMenu;
+ const { open, position, resource } = state.contextMenu;
return {
anchorEl: resource ? createAnchorAt(position) : undefined,
items: getMenuActionSet(resource),
+ open,
resource
};
};
export enum ContextMenuKind {
RootProject = "RootProject",
- Project = "Project"
+ Project = "Project",
+ Resource = "Resource"
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Dialog, DialogActions, DialogTitle, DialogContent, WithStyles, withStyles, StyleRulesCallback, Button, Typography, Paper } from '@material-ui/core';
+import { ArvadosTheme } from '../../common/custom-theme';
+
+type CssRules = 'link' | 'paper' | 'button';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ link: {
+ color: theme.palette.primary.main,
+ textDecoration: 'none',
+ margin: '0px 4px'
+ },
+ paper: {
+ padding: theme.spacing.unit,
+ marginBottom: theme.spacing.unit * 2,
+ backgroundColor: theme.palette.grey["200"],
+ border: `1px solid ${theme.palette.grey["300"]}`
+ },
+ button: {
+ fontSize: '0.8125rem',
+ fontWeight: 600
+ }
+});
+
+interface CurrentTokenDataProps {
+ currentToken?: string;
+ open: boolean;
+}
+
+interface CurrentTokenActionProps {
+ handleClose: () => void;
+}
+
+type CurrentTokenProps = CurrentTokenDataProps & CurrentTokenActionProps & WithStyles<CssRules>;
+
+export const CurrentTokenDialog = withStyles(styles)(
+ class extends React.Component<CurrentTokenProps> {
+
+ render() {
+ const { classes, open, handleClose, currentToken } = this.props;
+ return (
+ <Dialog open={open} onClose={handleClose} fullWidth={true} maxWidth='md'>
+ <DialogTitle>Current Token</DialogTitle>
+ <DialogContent>
+ <Typography variant='body1' paragraph={true}>
+ The Arvados API token is a secret key that enables the Arvados SDKs to access Arvados with the proper permissions.
+ <Typography component='p'>
+ For more information see
+ <a href='http://doc.arvados.org/user/reference/api-tokens.html' target='blank' className={classes.link}>
+ Getting an API token.
+ </a>
+ </Typography>
+ </Typography>
+
+ <Typography variant='body1' paragraph={true}>
+ Paste the following lines at a shell prompt to set up the necessary environment for Arvados SDKs to authenticate to your klingenc account.
+ </Typography>
+
+ <Paper className={classes.paper} elevation={0}>
+ <Typography variant='body1'>
+ HISTIGNORE=$HISTIGNORE:'export ARVADOS_API_TOKEN=*'
+ </Typography>
+ <Typography variant='body1'>
+ export ARVADOS_API_TOKEN={currentToken}
+ </Typography>
+ <Typography variant='body1'>
+ export ARVADOS_API_HOST=api.ardev.roche.com
+ </Typography>
+ <Typography variant='body1'>
+ unset ARVADOS_API_HOST_INSECURE
+ </Typography>
+ </Paper>
+ <Typography variant='body1'>
+ Arvados
+ <a href='http://doc.arvados.org/user/reference/api-tokens.html' target='blank' className={classes.link}>virtual machines</a>
+ do this for you automatically. This setup is needed only when you use the API remotely (e.g., from your own workstation).
+ </Typography>
+ </DialogContent>
+ <DialogActions>
+ <Button onClick={handleClose} className={classes.button} color="primary">CLOSE</Button>
+ </DialogActions>
+ </Dialog>
+ );
+ }
+ }
+);
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { FavoriteIcon } from "../../components/icon/icon";
+import { connect } from "react-redux";
+import { RootState } from "../../store/store";
+import { withStyles, StyleRulesCallback, WithStyles } from "@material-ui/core";
+
+type CssRules = "icon";
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+ icon: {
+ fontSize: "inherit"
+ }
+});
+
+const mapStateToProps = (state: RootState, props: { resourceUuid: string; className?: string; }) => ({
+ ...props,
+ visible: state.favorites[props.resourceUuid],
+});
+
+export const FavoriteStar = connect(mapStateToProps)(
+ withStyles(styles)((props: { visible: boolean; className?: string; } & WithStyles<CssRules>) =>
+ props.visible ? <FavoriteIcon className={props.className || props.classes.icon} /> : null
+ ));
\ No newline at end of file
user={user}
onContextMenu={jest.fn()}
onDetailsPanelToggle={jest.fn()}
- onContextMenu={jest.fn()}
{...{ searchText: "", breadcrumbs: [], menuItems: { accountMenu: [], helpMenu: [], anonymousMenu: [] }, onSearch: jest.fn(), onBreadcrumbClick: jest.fn(), onMenuItemClick: jest.fn() }}
/>
);
searchDebounce={2000}
onContextMenu={jest.fn()}
onSearch={onSearch}
- onContextMenu={jest.fn()}
onDetailsPanelToggle={jest.fn()}
{...{ user, breadcrumbs: [], menuItems: { accountMenu: [], helpMenu: [], anonymousMenu: [] }, onBreadcrumbClick: jest.fn(), onMenuItemClick: jest.fn() }}
/>
breadcrumbs={items}
onContextMenu={jest.fn()}
onBreadcrumbClick={onBreadcrumbClick}
- onContextMenu={jest.fn()}
onDetailsPanelToggle={jest.fn()}
{...{ user, searchText: "", menuItems: { accountMenu: [], helpMenu: [], anonymousMenu: [] }, onSearch: jest.fn(), onMenuItemClick: jest.fn() }}
/>
menuItems={menuItems}
onContextMenu={jest.fn()}
onMenuItemClick={onMenuItemClick}
- onContextMenu={jest.fn()}
onDetailsPanelToggle={jest.fn()}
{...{ user, searchText: "", breadcrumbs: [], onSearch: jest.fn(), onBreadcrumbClick: jest.fn() }}
/>
import { SortDirection } from '../../components/data-table/data-column';
import { ResourceKind } from '../../models/resource';
import { resourceLabel } from '../../common/labels';
-import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon } from '../../components/icon/icon';
+import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon, FavoriteIcon } from '../../components/icon/icon';
import { ArvadosTheme } from '../../common/custom-theme';
+import { FavoriteStar } from '../../views-components/favorite-star/favorite-star';
type CssRules = "toolbar" | "button";
{item.name}
</Typography>
</Grid>
+ <Grid item>
+ <Typography variant="caption">
+ <FavoriteStar resourceUuid={item.uuid} />
+ </Typography>
+ </Grid>
</Grid>;
}
type ProjectPanelProps = ProjectPanelDataProps & ProjectPanelActionProps & DispatchProp
- & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+ & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
export const ProjectPanel = withStyles(styles)(
connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))(
import { ProjectResource } from '../../models/project';
import { ResourceKind } from '../../models/resource';
import { ContextMenu, ContextMenuKind } from "../../views-components/context-menu/context-menu";
+import { CurrentTokenDialog } from '../../views-components/current-token-dialog/current-token-dialog';
const drawerWidth = 240;
const appBarHeight = 100;
projects: Array<TreeItem<ProjectResource>>;
currentProjectId: string;
user?: User;
+ currentToken?: string;
sidePanelItems: SidePanelItem[];
}
}
interface WorkbenchState {
+ isCurrentTokenDialogOpen: boolean;
anchorEl: any;
searchText: string;
menuItems: {
projects: state.projects.items,
currentProjectId: state.projects.currentItemId,
user: state.auth.user,
+ currentToken: state.auth.apiToken,
sidePanelItems: state.sidePanel
})
)(
class extends React.Component<WorkbenchProps, WorkbenchState> {
state = {
isCreationDialogOpen: false,
+ isCurrentTokenDialogOpen: false,
anchorEl: null,
searchText: "",
breadcrumbs: [],
menuItems: {
accountMenu: [
+ {
+ label: 'Current token',
+ action: () => this.toggleCurrentTokenModal()
+ },
{
label: "Logout",
action: () => this.props.dispatch(authActions.LOGOUT())
toggleOpen={this.toggleSidePanelOpen}
toggleActive={this.toggleSidePanelActive}
sidePanelItems={this.props.sidePanelItems}
- onContextMenu={(event) => this.openContextMenu(event, authService.getUuid() || "", ContextMenuKind.RootProject)}>
+ onContextMenu={(event) => this.openContextMenu(event, {
+ uuid: authService.getUuid() || "",
+ name: "",
+ kind: ContextMenuKind.RootProject
+ })}>
<ProjectTree
projects={this.props.projects}
toggleOpen={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.OPEN))}
- onContextMenu={(event, item) => this.openContextMenu(event, item.data.uuid, ContextMenuKind.Project)}
+ onContextMenu={(event, item) => this.openContextMenu(event, {
+ uuid: item.data.uuid,
+ name: item.data.name,
+ kind: ContextMenuKind.Project
+ })}
toggleActive={itemId => {
this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE));
this.props.dispatch<any>(loadDetails(itemId, ResourceKind.Project));
</main>
<ContextMenu />
<CreateProjectDialog />
+ <CurrentTokenDialog
+ currentToken={this.props.currentToken}
+ open={this.state.isCurrentTokenDialogOpen}
+ handleClose={this.toggleCurrentTokenModal} />
</div>
);
}
renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => <ProjectPanel
onItemRouteChange={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE))}
- onContextMenu={(event, item) => this.openContextMenu(event, item.uuid, ContextMenuKind.Project)}
+ onContextMenu={(event, item) => {
+ const kind = item.kind === ResourceKind.Project ? ContextMenuKind.Project : ContextMenuKind.Resource;
+ this.openContextMenu(event, {
+ uuid: item.uuid,
+ name: item.name,
+ kind
+ });
+ }}
onDialogOpen={this.handleCreationDialogOpen}
onItemClick={item => {
this.props.dispatch<any>(loadDetails(item.uuid, item.kind as ResourceKind));
this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
},
onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: NavBreadcrumb) => {
- this.openContextMenu(event, breadcrumb.itemId, ContextMenuKind.Project);
+ this.openContextMenu(event, {
+ uuid: breadcrumb.itemId,
+ name: breadcrumb.label,
+ kind: ContextMenuKind.Project
+ });
}
};
this.props.dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: itemUuid }));
}
- openContextMenu = (event: React.MouseEvent<HTMLElement>, itemUuid: string, kind: ContextMenuKind) => {
+ openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: { name: string; uuid: string; kind: ContextMenuKind; }) => {
event.preventDefault();
this.props.dispatch(
contextMenuActions.OPEN_CONTEXT_MENU({
position: { x: event.clientX, y: event.clientY },
- resource: { uuid: itemUuid, kind }
+ resource
})
);
}
+
+ toggleCurrentTokenModal = () => {
+ this.setState({ isCurrentTokenDialogOpen: !this.state.isCurrentTokenDialogOpen });
+ }
}
)
);