import { MoreOptionsIcon } from '~/components/icon/icon';
import { PaperProps } from '@material-ui/core/Paper';
-type CssRules = 'searchBox' | "toolbar" | "footer" | "root" | 'moreOptionsButton';
+type CssRules = 'searchBox' | "toolbar" | "toolbarUnderTitle" | "footer" | "root" | 'moreOptionsButton' | 'title';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
searchBox: {
toolbar: {
paddingTop: theme.spacing.unit * 2
},
+ toolbarUnderTitle: {
+ paddingTop: 0
+ },
footer: {
overflow: 'auto'
},
},
moreOptionsButton: {
padding: 0
+ },
+ title: {
+ paddingLeft: theme.spacing.unit * 3,
+ paddingTop: theme.spacing.unit * 3,
+ fontSize: '18px'
}
});
paperProps?: PaperProps;
actions?: React.ReactNode;
hideSearchInput?: boolean;
- header?: React.ReactNode;
+ title?: React.ReactNode;
paperKey?: string;
currentItemUuid: string;
}
rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch,
items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
- paperKey, fetchMode, currentItemUuid, header
+ paperKey, fetchMode, currentItemUuid, title
} = this.props;
return <Paper className={classes.root} {...paperProps} key={paperKey}>
- {(!hideColumnSelector || !hideSearchInput) && <Toolbar className={classes.toolbar}>
+ {title && <div className={classes.title}>{title}</div>}
+ {(!hideColumnSelector || !hideSearchInput) && <Toolbar className={title ? classes.toolbarUnderTitle : classes.toolbar}>
<Grid container justify="space-between" wrap="nowrap" alignItems="center">
- {header && <div>{header}</div>}
<div className={classes.searchBox}>
{!hideSearchInput && <SearchInput
value={searchValue}
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
import { ArvadosTheme } from '~/common/custom-theme';
import * as classnames from "classnames";
+import { Link } from 'react-router-dom';
type CssRules = 'attribute' | 'label' | 'value' | 'lowercaseValue' | 'link';
link?: string;
children?: React.ReactNode;
onValueClick?: () => void;
+ linkInsideCard?: string;
}
type DetailsAttributeProps = DetailsAttributeDataProps & WithStyles<CssRules>;
export const DetailsAttribute = withStyles(styles)(
- ({ label, link, value, children, classes, classLabel, classValue, lowercaseValue, onValueClick }: DetailsAttributeProps) =>
+ ({ label, link, value, children, classes, classLabel, classValue, lowercaseValue, onValueClick, linkInsideCard }: DetailsAttributeProps) =>
<Typography component="div" className={classes.attribute}>
<Typography component="span" className={classnames([classes.label, classLabel])}>{label}</Typography>
- { link
- ? <a href={link} className={classes.link} target='_blank'>{value}</a>
- : <Typography
- onClick={onValueClick}
- component="span"
- className={classnames([classes.value, classValue, { [classes.lowercaseValue]: lowercaseValue }])}
- >
- {value}
- {children}
- </Typography> }
+ {link && <a href={link} className={classes.link} target='_blank'>{value}</a>}
+ {linkInsideCard && <Link to={`/collections/${linkInsideCard}`} className={classes.link}>{value}</Link>}
+ {!link && !linkInsideCard && <Typography
+ onClick={onValueClick}
+ component="span"
+ className={classnames([classes.value, classValue, { [classes.lowercaseValue]: lowercaseValue }])}>
+ {value}
+ {children}
+ </Typography>
+ }
+
</Typography>
);
const projectMatch = Routes.matchProjectRoute(pathname);
const collectionMatch = Routes.matchCollectionRoute(pathname);
const favoriteMatch = Routes.matchFavoritesRoute(pathname);
- const publicFavoritesMatch = Routes.matchPublicFavorites(pathname);
+ const publicFavoritesMatch = Routes.matchPublicFavoritesRoute(pathname);
const trashMatch = Routes.matchTrashRoute(pathname);
const processMatch = Routes.matchProcessRoute(pathname);
const processLogMatch = Routes.matchProcessLogRoute(pathname);
const groupsMatch = Routes.matchGroupsRoute(pathname);
const groupDetailsMatch = Routes.matchGroupDetailsRoute(pathname);
const linksMatch = Routes.matchLinksRoute(pathname);
+ const collectionsContentAddressMatch = Routes.matchCollectionsContentAddressRoute(pathname);
store.dispatch(dialogActions.CLOSE_ALL_DIALOGS());
store.dispatch(contextMenuActions.CLOSE_CONTEXT_MENU());
store.dispatch(WorkbenchActions.loadGroupDetailsPanel(groupDetailsMatch.params.id));
} else if (linksMatch) {
store.dispatch(WorkbenchActions.loadLinks);
+ } else if (collectionsContentAddressMatch) {
+ store.dispatch(WorkbenchActions.loadCollectionContentAddress);
}
};
\ No newline at end of file
GROUPS: '/groups',
GROUP_DETAILS: `/group/:id(${RESOURCE_UUID_PATTERN})`,
LINKS: '/links',
- PUBLIC_FAVORITES: '/public-favorites'
+ PUBLIC_FAVORITES: '/public-favorites',
+ COLLECTIONS_CONTENT_ADDRESS: '/collections/:id',
};
export const getResourceUrl = (uuid: string) => {
export const matchLinksRoute = (route: string) =>
matchPath(route, { path: Routes.LINKS });
-export const matchPublicFavorites = (route: string) =>
+export const matchPublicFavoritesRoute = (route: string) =>
matchPath(route, { path: Routes.PUBLIC_FAVORITES });
+
+export const matchCollectionsContentAddressRoute = (route: string) =>
+ matchPath(route, { path: Routes.COLLECTIONS_CONTENT_ADDRESS });
import { CollectionResource } from '~/models/collection';
import { collectionPanelFilesAction } from "./collection-panel-files/collection-panel-files-actions";
import { createTree } from "~/models/tree";
-import { RootState } from "../store";
+import { RootState } from "~/store/store";
import { ServiceRepository } from "~/services/services";
import { TagProperty } from "~/models/tag";
import { snackbarActions } from "../snackbar/snackbar-actions";
import { unionize, ofType, UnionOf } from '~/common/unionize';
import { SnackbarKind } from '~/store/snackbar/snackbar-actions';
import { navigateTo } from '~/store/navigation/navigation-action';
+import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
export const collectionPanelActions = unionize({
SET_COLLECTION: ofType<CollectionResource>(),
dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid }));
dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() }));
const collection = await services.collectionService.get(uuid);
+ dispatch(loadDetailsPanel(collection.uuid));
dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: collection }));
dispatch(resourcesActions.SET_RESOURCES([collection]));
dispatch<any>(loadCollectionFiles(collection.uuid));
? 'Are you sure you want to remove this directory?'
: 'Are you sure you want to remove this file?';
const info = isDirectory
- ? 'Removing files will change content adress.'
- : 'Removing a file will change content adress.';
+ ? 'Removing files will change content address.'
+ : 'Removing a file will change content address.';
dispatch(dialogActions.OPEN_DIALOG({
id: FILE_REMOVE_DIALOG,
data: {
title: 'Removing files',
text: 'Are you sure you want to remove selected files?',
- info: 'Removing files will change content adress.',
+ info: 'Removing files will change content address.',
confirmButtonLabel: 'Remove'
}
});
collectionPanelActions.match(action, {
default: () => state,
SET_COLLECTION: (item) => ({ ...state, item }),
- LOAD_COLLECTION_SUCCESS: ({ item }) => ({ ...state, item }),
+ LOAD_COLLECTION_SUCCESS: ({ item }) => ({ ...state, item })
});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ServiceRepository } from '~/services/services';
+import { MiddlewareAPI, Dispatch } from 'redux';
+import { DataExplorerMiddlewareService } from '~/store/data-explorer/data-explorer-middleware-service';
+import { RootState } from '~/store/store';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
+import { resourcesActions } from '~/store/resources/resources-actions';
+import { FilterBuilder } from '~/services/api/filter-builder';
+import { SortDirection } from '~/components/data-table/data-column';
+import { OrderDirection, OrderBuilder } from '~/services/api/order-builder';
+import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
+import { FavoritePanelColumnNames } from '~/views/favorite-panel/favorite-panel';
+import { GroupContentsResource, GroupContentsResourcePrefix } from '~/services/groups-service/groups-service';
+import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
+import { collectionsContentAddressActions } from './collections-content-address-panel-actions';
+import { navigateTo } from '~/store/navigation/navigation-action';
+import { updateFavorites } from '~/store/favorites/favorites-actions';
+import { updatePublicFavorites } from '~/store/public-favorites/public-favorites-actions';
+import { setBreadcrumbs } from '../breadcrumbs/breadcrumbs-actions';
+import { ResourceKind, extractUuidKind } from '~/models/resource';
+import { ownerNameActions } from '~/store/owner-name/owner-name-actions';
+
+export class CollectionsWithSameContentAddressMiddlewareService extends DataExplorerMiddlewareService {
+ constructor(private services: ServiceRepository, id: string) {
+ super(id);
+ }
+
+ async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+ const dataExplorer = getDataExplorer(api.getState().dataExplorer, this.getId());
+ if (!dataExplorer) {
+ api.dispatch(collectionPanelDataExplorerIsNotSet());
+ } else {
+ const sortColumn = getSortColumn(dataExplorer);
+
+ const contentOrder = new OrderBuilder<GroupContentsResource>();
+
+ if (sortColumn && sortColumn.name === FavoritePanelColumnNames.NAME) {
+ const direction = sortColumn.sortDirection === SortDirection.ASC
+ ? OrderDirection.ASC
+ : OrderDirection.DESC;
+
+ contentOrder
+ .addOrder(direction, "name", GroupContentsResourcePrefix.COLLECTION);
+ }
+ try {
+ api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
+ const userUuid = api.getState().auth.user!.uuid;
+ const pathname = api.getState().router.location!.pathname;
+ const contentAddress = pathname.split('/')[2];
+ const response = await this.services.collectionService.list({
+ limit: dataExplorer.rowsPerPage,
+ offset: dataExplorer.page * dataExplorer.rowsPerPage,
+ filters: new FilterBuilder()
+ .addEqual('portableDataHash', contentAddress)
+ .addILike("name", dataExplorer.searchValue)
+ .getFilters()
+ });
+ const userUuids = response.items.map(it => {
+ if (extractUuidKind(it.ownerUuid) === ResourceKind.USER) {
+ return it.ownerUuid;
+ } else {
+ return '';
+ }
+ }
+ );
+ const groupUuids = response.items.map(it => {
+ if (extractUuidKind(it.ownerUuid) === ResourceKind.GROUP) {
+ return it.ownerUuid;
+ } else {
+ return '';
+ }
+ });
+ const responseUsers = await this.services.userService.list({
+ limit: dataExplorer.rowsPerPage,
+ offset: dataExplorer.page * dataExplorer.rowsPerPage,
+ filters: new FilterBuilder()
+ .addIn('uuid', userUuids)
+ .getFilters()
+ });
+ const responseGroups = await this.services.groupsService.list({
+ limit: dataExplorer.rowsPerPage,
+ offset: dataExplorer.page * dataExplorer.rowsPerPage,
+ filters: new FilterBuilder()
+ .addIn('uuid', groupUuids)
+ .getFilters()
+ });
+ responseUsers.items.map(it=>{
+ api.dispatch<any>(ownerNameActions.SET_OWNER_NAME({name: it.uuid === userUuid ? 'User: Me' : `User: ${it.firstName} ${it.lastName}`, uuid: it.uuid}));
+ });
+ responseGroups.items.map(it=>{
+ api.dispatch<any>(ownerNameActions.SET_OWNER_NAME({name: `Project: ${it.name}`, uuid: it.uuid}));
+ });
+ api.dispatch<any>(setBreadcrumbs([{ label: 'Projects', uuid: userUuid }]));
+ api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
+ api.dispatch<any>(updatePublicFavorites(response.items.map(item => item.uuid)));
+ if (response.itemsAvailable === 1) {
+ api.dispatch<any>(navigateTo(response.items[0].uuid));
+ api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+ } else {
+ api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+ api.dispatch(resourcesActions.SET_RESOURCES(response.items));
+ api.dispatch(collectionsContentAddressActions.SET_ITEMS({
+ items: response.items.map((resource: any) => resource.uuid),
+ itemsAvailable: response.itemsAvailable,
+ page: Math.floor(response.offset / response.limit),
+ rowsPerPage: response.limit
+ }));
+ }
+ } catch (e) {
+ api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+ api.dispatch(collectionsContentAddressActions.SET_ITEMS({
+ items: [],
+ itemsAvailable: 0,
+ page: 0,
+ rowsPerPage: dataExplorer.rowsPerPage
+ }));
+ api.dispatch(couldNotFetchCollections());
+ }
+ }
+ }
+}
+
+const collectionPanelDataExplorerIsNotSet = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Collection panel is not ready.',
+ kind: SnackbarKind.ERROR
+ });
+
+const couldNotFetchCollections = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Could not fetch collection with this content address.',
+ kind: SnackbarKind.ERROR
+ });
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
+
+export const COLLECTIONS_CONTENT_ADDRESS_PANEL_ID = 'collectionsContentAddressPanel';
+
+export const collectionsContentAddressActions = bindDataExplorerActions(COLLECTIONS_CONTENT_ADDRESS_PANEL_ID);
+
+export const loadCollectionsContentAddressPanel = () =>
+ (dispatch: Dispatch) => {
+ dispatch(collectionsContentAddressActions.REQUEST_ITEMS());
+ };
export const navigateToGroupDetails = compose(push, getGroupUrl);
export const navigateToLinks = push(Routes.LINKS);
+
+export const navigateToCollectionsContentAddress = push(Routes.COLLECTIONS_CONTENT_ADDRESS);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { unionize, ofType, UnionOf } from '~/common/unionize';
+
+export const ownerNameActions = unionize({
+ SET_OWNER_NAME: ofType<OwnerNameState>()
+});
+
+interface OwnerNameState {
+ name: string;
+ uuid: string;
+}
+
+export type OwnerNameAction = UnionOf<typeof ownerNameActions>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ownerNameActions, OwnerNameAction } from './owner-name-actions';
+
+export const ownerNameReducer = (state = [], action: OwnerNameAction) =>
+ ownerNameActions.match(action, {
+ SET_OWNER_NAME: data => [...state, { uuid: data.uuid, name: data.name }],
+ default: () => state,
+ });
\ No newline at end of file
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
+import { projectPanelActions } from '~/store/project-panel/project-panel-action';
+import { loadSidePanelTreeProjects } from '../side-panel-tree/side-panel-tree-actions';
export const PROJECT_MOVE_FORM_NAME = 'projectMoveFormName';
export const moveProject = (resource: MoveToFormDialogData) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const userUuid = getState().auth.user!.uuid;
dispatch(startSubmit(PROJECT_MOVE_FORM_NAME));
try {
const project = await services.projectService.get(resource.uuid);
const newProject = await services.projectService.update(resource.uuid, { ...project, ownerUuid: resource.ownerUuid });
+ dispatch(projectPanelActions.REQUEST_ITEMS());
dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_MOVE_FORM_NAME }));
+ await dispatch<any>(loadSidePanelTreeProjects(userUuid));
return newProject;
} catch (e) {
const error = getCommonResourceServiceError(e);
import { ProjectResource } from '~/models/project';
import { ContextMenuResource } from "~/store/context-menu/context-menu-actions";
import { getResource } from '~/store/resources/resources';
+import { projectPanelActions } from '~/store/project-panel/project-panel-action';
export interface ProjectUpdateFormDialogData {
uuid: string;
dispatch(startSubmit(PROJECT_UPDATE_FORM_NAME));
try {
const updatedProject = await services.projectService.update(uuid, project);
+ dispatch(projectPanelActions.REQUEST_ITEMS());
dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_UPDATE_FORM_NAME }));
return updatedProject;
} catch (e) {
import { PublicFavoritesMiddlewareService } from '~/store/public-favorites-panel/public-favorites-middleware-service';
import { PUBLIC_FAVORITE_PANEL_ID } from '~/store/public-favorites-panel/public-favorites-action';
import { publicFavoritesReducer } from '~/store/public-favorites/public-favorites-reducer';
+import { CollectionsWithSameContentAddressMiddlewareService } from '~/store/collections-content-address-panel/collections-content-address-middleware-service';
+import { COLLECTIONS_CONTENT_ADDRESS_PANEL_ID } from '~/store/collections-content-address-panel/collections-content-address-panel-actions';
+import { ownerNameReducer } from '~/store/owner-name/owner-name-reducer';
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
const publicFavoritesMiddleware = dataExplorerMiddleware(
new PublicFavoritesMiddlewareService(services, PUBLIC_FAVORITE_PANEL_ID)
);
+ const collectionsContentAddress = dataExplorerMiddleware(
+ new CollectionsWithSameContentAddressMiddlewareService(services, COLLECTIONS_CONTENT_ADDRESS_PANEL_ID)
+ );
+
const middlewares: Middleware[] = [
routerMiddleware(history),
thunkMiddleware.withExtraArgument(services),
linkPanelMiddleware,
computeNodeMiddleware,
apiClientAuthorizationMiddlewareService,
- publicFavoritesMiddleware
+ publicFavoritesMiddleware,
+ collectionsContentAddress
];
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
return createStore(rootReducer, enhancer);
detailsPanel: detailsPanelReducer,
dialog: dialogReducer,
favorites: favoritesReducer,
+ ownerName: ownerNameReducer,
publicFavorites: publicFavoritesReducer,
form: formReducer,
processLogsPanel: processLogsPanelReducer,
import { GroupContentsResource } from '~/services/groups-service/groups-service';
import { MatchCases, ofType, unionize, UnionOf } from '~/common/unionize';
import { loadRunProcessPanel } from '~/store/run-process-panel/run-process-panel-actions';
-import { loadCollectionFiles } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions';
-import { collectionPanelActions } from "~/store/collection-panel/collection-panel-action";
+import { collectionPanelActions, loadCollectionPanel } from "~/store/collection-panel/collection-panel-action";
import { CollectionResource } from "~/models/collection";
import {
loadSearchResultsPanel,
import { DataTableFetchMode } from "~/components/data-table/data-table";
import { loadPublicFavoritePanel, publicFavoritePanelActions } from '~/store/public-favorites-panel/public-favorites-action';
import { publicFavoritePanelColumns } from '~/views/public-favorites-panel/public-favorites-panel';
+import { loadCollectionsContentAddressPanel, collectionsContentAddressActions } from '~/store/collections-content-address-panel/collections-content-address-panel-actions';
+import { collectionContentAddressPanelColumns } from '~/views/collection-content-address-panel/collection-content-address-panel';
export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
dispatch(computeNodesActions.SET_COLUMNS({ columns: computeNodePanelColumns }));
dispatch(apiClientAuthorizationsActions.SET_COLUMNS({ columns: apiClientAuthorizationPanelColumns }));
+ dispatch(collectionsContentAddressActions.SET_COLUMNS({ columns: collectionContentAddressPanelColumns }));
dispatch<any>(initSidePanelTree());
if (router.location) {
dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
});
+export const loadCollectionContentAddress = handleFirstTimeLoad(
+ async (dispatch: Dispatch<any>) => {
+ await dispatch(loadCollectionsContentAddressPanel());
+ });
+
export const loadTrash = () =>
handleFirstTimeLoad(
(dispatch: Dispatch) => {
dispatch(updateResources([collection]));
await dispatch(activateSidePanelTreeItem(collection.ownerUuid));
dispatch(setSidePanelBreadcrumbs(collection.ownerUuid));
- dispatch(loadCollectionFiles(collection.uuid));
+ dispatch(loadCollectionPanel(collection.uuid));
},
SHARED: collection => {
dispatch(collectionPanelActions.SET_COLLECTION(collection as CollectionResource));
dispatch(updateResources([collection]));
dispatch<any>(setSharedWithMeBreadcrumbs(collection.ownerUuid));
dispatch(activateSidePanelTreeItem(collection.ownerUuid));
- dispatch(loadCollectionFiles(collection.uuid));
+ dispatch(loadCollectionPanel(collection.uuid));
},
TRASHED: collection => {
dispatch(collectionPanelActions.SET_COLLECTION(collection as CollectionResource));
dispatch(updateResources([collection]));
dispatch(setTrashBreadcrumbs(''));
dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
- dispatch(loadCollectionFiles(collection.uuid));
+ dispatch(loadCollectionPanel(collection.uuid));
},
});
import { navigateTo } from '~/store/navigation/navigation-action';
import { withResourceData } from '~/views-components/data-explorer/with-resources';
-const renderName = (item: { name: string; uuid: string, kind: string }) =>
+const renderName = (dispatch: Dispatch, item: { name: string; uuid: string, kind: string }) =>
<Grid container alignItems="center" wrap="nowrap" spacing={16}>
<Grid item>
{renderIcon(item.kind)}
</Grid>
<Grid item>
- <Typography color="primary" style={{ width: 'auto' }}>
+ <Typography color="primary" style={{ width: 'auto', cursor: 'pointer' }} onClick={() => dispatch<any>(navigateTo(item.uuid))}>
{item.name}
</Typography>
</Grid>
(state: RootState, props: { uuid: string }) => {
const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
return resource || { name: '', uuid: '', kind: '' };
- })(renderName);
+ })((resource: { name: string; uuid: string, kind: string } & DispatchProp<any>) => renderName(resource.dispatch, resource));
const renderIcon = (kind: string) => {
switch (kind) {
})((props: { fileSize?: number }) => renderFileSize(props.fileSize));
const renderOwner = (owner: string) =>
- <Typography noWrap color="primary" >
+ <Typography noWrap>
{owner}
</Typography>;
return { owner: resource ? resource.ownerUuid : '' };
})((props: { owner: string }) => renderOwner(props.owner));
+export const ResourceOwnerName = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
+ const ownerNameState = state.ownerName;
+ const ownerName = ownerNameState.find(it => it.uuid === resource!.ownerUuid);
+ return { owner: ownerName ? ownerName!.name : resource!.ownerUuid };
+ })((props: { owner: string }) => renderOwner(props.owner));
+
const renderType = (type: string) =>
<Typography noWrap>
{resourceLabel(type)}
<DetailsAttribute label='Owner' value={this.item.ownerUuid} lowercaseValue={true} />
<DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
<DetailsAttribute label='Created at' value={formatDate(this.item.createdAt)} />
- {/* Links but we dont have view */}
- <DetailsAttribute label='Collection UUID' link={this.item.uuid} value={this.item.uuid} />
- <DetailsAttribute label='Content address' link={this.item.portableDataHash} value={this.item.portableDataHash} />
+ <DetailsAttribute label='Collection UUID' linkInsideCard={this.item.uuid} value={this.item.uuid} />
+ <DetailsAttribute label='Content address' linkInsideCard={this.item.portableDataHash} value={this.item.portableDataHash} />
{/* Missing attrs */}
<DetailsAttribute label='Number of files' value={this.data && this.data.fileCount} />
<DetailsAttribute label='Content size' value={formatFileSize(this.data && this.data.fileSize)} />
const resourceData = getResourceData(detailsPanel.resourceUuid)(resourcesData);
return {
isOpened: detailsPanel.isOpened,
- item: getItem(resource || (file && file.value) || EMPTY_RESOURCE, resourceData)
+ item: getItem(resource || (file && file.value) || EMPTY_RESOURCE, resourceData),
};
};
import { FavoriteIcon, PublicFavoriteIcon } from "~/components/icon/icon";
import { connect } from "react-redux";
import { RootState } from "~/store/store";
-import { withStyles, StyleRulesCallback, WithStyles } from "@material-ui/core";
+import { withStyles, StyleRulesCallback, WithStyles, Tooltip } from "@material-ui/core";
type CssRules = "icon";
export const FavoriteStar = connect(mapStateToProps)(
withStyles(styles)((props: { isFavoriteVisible: boolean; className?: string; } & WithStyles<CssRules>) => {
if (props.isFavoriteVisible) {
- return <FavoriteIcon className={props.className || props.classes.icon} />;
+ return <Tooltip enterDelay={500} title="Favorite"><FavoriteIcon className={props.className || props.classes.icon} /></Tooltip>;
} else {
return null;
}
export const PublicFavoriteStar = connect(mapStateToProps)(
withStyles(styles)((props: { isPublicFavoriteVisible: boolean; className?: string; } & WithStyles<CssRules>) => {
if (props.isPublicFavoriteVisible) {
- return <PublicFavoriteIcon className={props.className || props.classes.icon} />;
+ return <Tooltip enterDelay={500} title="Public Favorite"><PublicFavoriteIcon className={props.className || props.classes.icon} /></Tooltip>;
} else {
return null;
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { StyleRulesCallback, WithStyles, withStyles, Grid, Button } from '@material-ui/core';
+import { CollectionIcon } from '~/components/icon/icon';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { BackIcon } from '~/components/icon/icon';
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+import { COLLECTIONS_CONTENT_ADDRESS_PANEL_ID } from '~/store/collections-content-address-panel/collections-content-address-panel-actions';
+import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
+import { Dispatch } from 'redux';
+import { getIsAdmin } from '~/store/public-favorites/public-favorites-actions';
+import { resourceKindToContextMenuKind, openContextMenu } from '~/store/context-menu/context-menu-actions';
+import { ResourceKind } from '~/models/resource';
+import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
+import { connect } from 'react-redux';
+import { navigateTo } from '~/store/navigation/navigation-action';
+import { DataColumns } from '~/components/data-table/data-table';
+import { SortDirection } from '~/components/data-table/data-column';
+import { createTree } from '~/models/tree';
+import { ResourceName, ResourceOwnerName, ResourceLastModifiedDate } from '~/views-components/data-explorer/renderers';
+
+type CssRules = 'backLink' | 'backIcon' | 'card' | 'title' | 'iconHeader' | 'link';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ backLink: {
+ fontSize: '14px',
+ fontWeight: 600,
+ display: 'flex',
+ alignItems: 'center',
+ padding: theme.spacing.unit,
+ marginBottom: theme.spacing.unit,
+ color: theme.palette.grey["700"],
+ },
+ backIcon: {
+ marginRight: theme.spacing.unit
+ },
+ card: {
+ width: '100%'
+ },
+ title: {
+ color: theme.palette.grey["700"]
+ },
+ iconHeader: {
+ fontSize: '1.875rem',
+ color: theme.customs.colors.green700
+ },
+ link: {
+ fontSize: '0.875rem',
+ color: theme.palette.primary.main,
+ textAlign: 'right',
+ '&:hover': {
+ cursor: 'pointer'
+ }
+ }
+});
+
+enum CollectionContentAddressPanelColumnNames {
+ COLLECTION_WITH_THIS_ADDRESS = "Collection with this address",
+ LOCATION = "Location",
+ LAST_MODIFIED = "Last modified"
+}
+
+export const collectionContentAddressPanelColumns: DataColumns<string> = [
+ {
+ name: CollectionContentAddressPanelColumnNames.COLLECTION_WITH_THIS_ADDRESS,
+ selected: true,
+ configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: createTree(),
+ render: uuid => <ResourceName uuid={uuid} />
+ },
+ {
+ name: CollectionContentAddressPanelColumnNames.LOCATION,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <ResourceOwnerName uuid={uuid} />
+ },
+ {
+ name: CollectionContentAddressPanelColumnNames.LAST_MODIFIED,
+ selected: true,
+ configurable: true,
+ sortDirection: SortDirection.DESC,
+ filters: createTree(),
+ render: uuid => <ResourceLastModifiedDate uuid={uuid} />
+ }
+];
+
+export interface CollectionContentAddressPanelActionProps {
+ onContextMenu: (event: React.MouseEvent<any>, uuid: string) => void;
+ onItemClick: (item: string) => void;
+ onItemDoubleClick: (item: string) => void;
+}
+
+const mapDispatchToProps = (dispatch: Dispatch): CollectionContentAddressPanelActionProps => ({
+ onContextMenu: (event, resourceUuid) => {
+ const isAdmin = dispatch<any>(getIsAdmin());
+ const kind = resourceKindToContextMenuKind(resourceUuid, isAdmin);
+ if (kind) {
+ dispatch<any>(openContextMenu(event, {
+ name: '',
+ uuid: resourceUuid,
+ ownerUuid: '',
+ kind: ResourceKind.NONE,
+ menuKind: kind
+ }));
+ }
+ dispatch<any>(loadDetailsPanel(resourceUuid));
+ },
+ onItemClick: (uuid: string) => {
+ dispatch<any>(loadDetailsPanel(uuid));
+ },
+ onItemDoubleClick: uuid => {
+ dispatch<any>(navigateTo(uuid));
+ }
+});
+
+interface CollectionContentAddressDataProps {
+ match: {
+ params: { id: string }
+ };
+}
+
+export const CollectionsContentAddressPanel = withStyles(styles)(
+ connect(null, mapDispatchToProps)(
+ class extends React.Component<CollectionContentAddressPanelActionProps & CollectionContentAddressDataProps & WithStyles<CssRules>> {
+ render() {
+ return <Grid item xs={12}>
+ <Button
+ onClick={() => history.back()}
+ className={this.props.classes.backLink}>
+ <BackIcon className={this.props.classes.backIcon} />
+ Back
+ </Button>
+ <DataExplorer
+ id={COLLECTIONS_CONTENT_ADDRESS_PANEL_ID}
+ onRowClick={this.props.onItemClick}
+ onRowDoubleClick={this.props.onItemDoubleClick}
+ onContextMenu={this.props.onContextMenu}
+ contextMenuColumn={true}
+ title={`Content address: ${this.props.match.params.id}`}
+ dataTableDefaultView={
+ <DataTableDefaultView
+ icon={CollectionIcon}
+ messages={['Collections with this content address not found.']} />
+ } />;
+ </Grid >;
+ }
+ }
+ )
+);
ResourceType
} from '~/views-components/data-explorer/renderers';
import { FavoriteIcon } from '~/components/icon/icon';
-import { Dispatch } from 'redux';
import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
import { navigateTo } from '~/store/navigation/navigation-action';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
backLink: {
- fontSize: '1rem',
+ fontSize: '14px',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
({ classes, process, selectedFilter, filters, onChange, lines, onContextMenu, navigateToLogCollection }: ProcessLogMainCardProps & WithStyles<CssRules>) =>
<Grid item xs={12}>
<Link to={`/processes/${process.containerRequest.uuid}`} className={classes.backLink}>
- <BackIcon className={classes.backIcon} /> Back
+ <BackIcon className={classes.backIcon} /> BACK
</Link>
<Card className={classes.card}>
<CardHeader
onContextMenu={props.onContextMenu}
contextMenuColumn={true}
hideSearchInput
- header={
+ title={
props.localCluster === homeCluster ?
- <p>Searching clusters: {props.sessions.filter((ss) => ss.loggedIn).map((ss) => <span key={ss.clusterId}> {ss.clusterId}</span>)}</p> :
- <p>Searching local cluster {props.localCluster} only. To search multiple clusters, <a href={props.remoteHostsConfig[homeCluster] && props.remoteHostsConfig[homeCluster].workbench2Url}> start from your home Workbench.</a></p>
+ <div>Searching clusters: {props.sessions.filter((ss) => ss.loggedIn).map((ss) => <span key={ss.clusterId}> {ss.clusterId}</span>)}</div> :
+ <div>Searching local cluster {props.localCluster} only. To search multiple clusters, <a href={props.remoteHostsConfig[homeCluster] && props.remoteHostsConfig[homeCluster].workbench2Url}> start from your home Workbench.</a></div>
}
/>;
};
import { PartialCopyToCollectionDialog } from '~/views-components/dialog-forms/partial-copy-to-collection-dialog';
import { PublicFavoritePanel } from '~/views/public-favorites-panel/public-favorites-panel';
import { FedLogin } from './fed-login';
+import { CollectionsContentAddressPanel } from '~/views/collection-content-address-panel/collection-content-address-panel';
type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
<Route path={Routes.GROUP_DETAILS} component={GroupDetailsPanel} />
<Route path={Routes.LINKS} component={LinkPanel} />
<Route path={Routes.PUBLIC_FAVORITES} component={PublicFavoritePanel} />
+ <Route path={Routes.COLLECTIONS_CONTENT_ADDRESS} component={CollectionsContentAddressPanel} />
</Switch>
</Grid>
</Grid>