version: string;
websocketUrl: string;
workbenchUrl: string;
+ workbench2Url?: string;
vocabularyUrl: string;
fileViewersConfigUrl: string;
}
});
export const DISCOVERY_URL = 'discovery/v1/apis/arvados/v1/rest';
-const getDiscoveryURL = (apiHost: string) => `${window.location.protocol}//${apiHost}/${DISCOVERY_URL}`;
+export const getDiscoveryURL = (apiHost: string) => `${window.location.protocol}//${apiHost}/${DISCOVERY_URL}?nocache=${(new Date()).getTime()}`;
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;
+ 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
+ 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">
<div className={classes.searchBox}>
{!hideSearchInput && <SearchInput
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>
);
import { ConnectedRouter } from "react-router-redux";
import { ApiToken } from "~/views-components/api-token/api-token";
import { initAuth } from "~/store/auth/auth-action";
-import { configActions } from "~/store/config/config-action";
import { createServices } from "~/services/services";
import { MuiThemeProvider } from '@material-ui/core/styles';
import { CustomTheme } from '~/common/custom-theme';
import { setCurrentTokenDialogApiHost } from '~/store/current-token-dialog/current-token-dialog-actions';
import { processResourceActionSet } from '~/views-components/context-menu/action-sets/process-resource-action-set';
import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
-import { setUuidPrefix } from '~/store/workflow-panel/workflow-panel-actions';
import { trashedCollectionActionSet } from '~/views-components/context-menu/action-sets/trashed-collection-action-set';
import { ContainerRequestState } from '~/models/container-request';
import { MountKind } from '~/models/mount-types';
store.dispatch(loadVocabulary);
store.dispatch(loadFileViewersConfig);
- const TokenComponent = (props: any) => <ApiToken authService={services.authService} config={config} {...props} />;
+ const TokenComponent = (props: any) => <ApiToken authService={services.authService} config={config} loadMainApp={true} {...props} />;
+ const FedTokenComponent = (props: any) => <ApiToken authService={services.authService} config={config} loadMainApp={false} {...props} />;
const MainPanelComponent = (props: any) => <MainPanel {...props} />;
const App = () =>
<ConnectedRouter history={history}>
<Switch>
<Route path={Routes.TOKEN} component={TokenComponent} />
+ <Route path={Routes.FED_LOGIN} component={FedTokenComponent} />
<Route path={Routes.ROOT} component={MainPanelComponent} />
</Switch>
</ConnectedRouter>
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
export const Routes = {
ROOT: '/',
TOKEN: '/token',
+ FED_LOGIN: '/fedtoken',
PROJECTS: `/projects/:id(${RESOURCE_UUID_PATTERN})`,
COLLECTIONS: `/collections/:id(${RESOURCE_UUID_PATTERN})`,
PROCESSES: `/processes/:id(${RESOURCE_UUID_PATTERN})`,
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 });
export const USER_IS_ACTIVE = 'isActive';
export const USER_USERNAME = 'username';
export const USER_PREFS = 'prefs';
+export const HOME_CLUSTER = 'homeCluster';
export interface UserDetailsResponse {
email: string;
public saveApiToken(token: string) {
localStorage.setItem(API_TOKEN_KEY, token);
+ localStorage.setItem(HOME_CLUSTER, token.split('/')[1].substr(0, 5));
}
public removeApiToken() {
return localStorage.getItem(API_TOKEN_KEY) || undefined;
}
+ public getHomeCluster() {
+ return localStorage.getItem(HOME_CLUSTER) || undefined;
+ }
+
public getUuid() {
return localStorage.getItem(USER_UUID_KEY) || undefined;
}
localStorage.removeItem(USER_PREFS);
}
- public login(uuidPrefix: string, homeCluster: string) {
+ public login(uuidPrefix: string, homeCluster: string, remoteHosts: { [key: string]: string }) {
const currentUrl = `${window.location.protocol}//${window.location.host}/token`;
- window.location.assign(`https://${homeCluster}/login?remote=${uuidPrefix}&return_to=${currentUrl}`);
+ const homeClusterHost = remoteHosts[homeCluster];
+ window.location.assign(`https://${homeClusterHost}/login?${uuidPrefix !== homeCluster ? "remote=" + uuidPrefix + "&" : ""}return_to=${currentUrl}`);
}
public logout() {
return resp.data.items[0].uuid;
};
-const getSaltedToken = (clusterId: string, tokenUuid: string, token: string) => {
+export const getSaltedToken = (clusterId: string, tokenUuid: string, token: string) => {
const shaObj = new jsSHA("SHA-1", "TEXT");
let secret = token;
if (token.startsWith("v2/")) {
import { SshKeyResource } from '~/models/ssh-key';
import { User, UserResource } from "~/models/user";
import { Session } from "~/models/session";
-import { Config } from '~/common/config';
+import { getDiscoveryURL, Config } from '~/common/config';
import { initSessions } from "~/store/auth/auth-action-session";
import { cancelLinking } from '~/store/link-account-panel/link-account-panel-actions';
import { matchTokenRoute } from '~/routes/routes';
+import Axios from "axios";
+import { AxiosError } from "axios";
export const authActions = unionize({
SAVE_API_TOKEN: ofType<string>(),
SET_SESSIONS: ofType<Session[]>(),
ADD_SESSION: ofType<Session>(),
REMOVE_SESSION: ofType<string>(),
- UPDATE_SESSION: ofType<Session>()
+ UPDATE_SESSION: ofType<Session>(),
+ REMOTE_CLUSTER_CONFIG: ofType<{ config: Config }>(),
});
function setAuthorizationHeader(services: ServiceRepository, token: string) {
const user = services.authService.getUser();
const token = services.authService.getApiToken();
+ const homeCluster = services.authService.getHomeCluster();
if (token) {
setAuthorizationHeader(services, token);
}
dispatch(authActions.CONFIG({ config }));
+ dispatch(authActions.SET_HOME_CLUSTER(homeCluster || config.uuidPrefix));
if (token && user) {
dispatch(authActions.INIT({ user, token }));
dispatch<any>(initSessions(services.authService, config, user));
dispatch<any>(getUserDetails()).then((user: User) => {
dispatch(authActions.INIT({ user, token }));
+ }).catch((err: AxiosError) => {
+ if (err.response) {
+ // Bad token
+ if (err.response.status === 401) {
+ logout()(dispatch, getState, services);
+ }
+ }
});
}
+ Object.keys(config.remoteHosts).map((k) => {
+ Axios.get<Config>(getDiscoveryURL(config.remoteHosts[k]))
+ .then(response => dispatch(authActions.REMOTE_CLUSTER_CONFIG({ config: response.data })));
+ });
};
export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(authActions.SAVE_USER(user));
};
-export const login = (uuidPrefix: string, homeCluster: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- services.authService.login(uuidPrefix, homeCluster);
+export const login = (uuidPrefix: string, homeCluster: string, remoteHosts: { [key: string]: string }) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ services.authService.login(uuidPrefix, homeCluster, remoteHosts);
dispatch(authActions.LOGIN());
};
import { ServiceRepository } from "~/services/services";
import { SshKeyResource } from '~/models/ssh-key';
import { Session } from "~/models/session";
+import { Config } from '~/common/config';
export interface AuthState {
user?: User;
localCluster: string;
homeCluster: string;
remoteHosts: { [key: string]: string };
+ remoteHostsConfig: { [key: string]: Config };
}
const initialState: AuthState = {
sessions: [],
localCluster: "",
homeCluster: "",
- remoteHosts: {}
+ remoteHosts: {},
+ remoteHostsConfig: {}
};
export const authReducer = (services: ServiceRepository) => (state = initialState, action: AuthAction) => {
homeCluster: config.uuidPrefix
};
},
+ REMOTE_CLUSTER_CONFIG: ({ config }) => {
+ return {
+ ...state,
+ remoteHostsConfig: { ...state.remoteHostsConfig, [config.uuidPrefix]: config },
+ };
+ },
INIT: ({ user, token }) => {
return { ...state, user, apiToken: token, homeCluster: user.uuid.substr(0, 5) };
},
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 openProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
(dispatch: Dispatch, getState: () => RootState) => {
const res = getResource<ProjectResource>(projectUuid)(getState().resources);
+ const isAdmin = getState().auth.user!.isAdmin;
if (res) {
dispatch<any>(openContextMenu(event, {
name: res.name,
uuid: res.uuid,
kind: res.kind,
- menuKind: ContextMenuKind.PROJECT,
+ menuKind: !isAdmin ? ContextMenuKind.PROJECT : ContextMenuKind.PROJECT_ADMIN,
ownerUuid: res.ownerUuid,
isTrashed: res.isTrashed
}));
}
try {
api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
- const resp1 = await this.services.linkService.list({
+ const responseLinks = await this.services.linkService.list({
filters: new FilterBuilder()
.addEqual("linkClass", 'star')
.addEqual('tailUuid', this.services.authService.getUuid()!)
.addEqual('tailKind', ResourceKind.USER)
.getFilters()
}).then(results => results);
- const uuids = resp1.items.map(it => it.headUuid);
+ const uuids = responseLinks.items.map(it => it.headUuid);
const groupItems: any = await this.services.groupsService.list({
filters: new FilterBuilder()
.addIn("uuid", uuids)
response.itemsAvailable++;
response.items.push(it);
});
-
+
api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
api.dispatch(resourcesActions.SET_RESOURCES(response.items));
await api.dispatch<any>(loadMissingProcessesInformation(response.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 { LinkResource, LinkClass } from '~/models/link';
import { GroupContentsResource, GroupContentsResourcePrefix } from '~/services/groups-service/groups-service';
import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
-import { loadMissingProcessesInformation } from '~/store/project-panel/project-panel-middleware-service';
import { updatePublicFavorites } from '~/store/public-favorites/public-favorites-actions';
export class PublicFavoritesMiddlewareService extends DataExplorerMiddlewareService {
api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
const uuidPrefix = api.getState().config.uuidPrefix;
const uuid = `${uuidPrefix}-j7d0g-fffffffffffffff`;
- const response = await this.services.linkService.list({
+ const responseLinks = await this.services.linkService.list({
limit: dataExplorer.rowsPerPage,
offset: dataExplorer.page * dataExplorer.rowsPerPage,
filters: new FilterBuilder()
.addIsA("headUuid", typeFilters)
.getFilters()
});
+ const uuids = responseLinks.items.map(it => it.headUuid);
+ const groupItems: any = await this.services.groupsService.list({
+ filters: new FilterBuilder()
+ .addIn("uuid", uuids)
+ .addILike("name", dataExplorer.searchValue)
+ .addIsA("uuid", typeFilters)
+ .getFilters()
+ });
+ const collectionItems: any = await this.services.collectionService.list({
+ filters: new FilterBuilder()
+ .addIn("uuid", uuids)
+ .addILike("name", dataExplorer.searchValue)
+ .addIsA("uuid", typeFilters)
+ .getFilters()
+ });
+ const processItems: any = await this.services.containerRequestService.list({
+ filters: new FilterBuilder()
+ .addIn("uuid", uuids)
+ .addILike("name", dataExplorer.searchValue)
+ .addIsA("uuid", typeFilters)
+ .getFilters()
+ });
+ const response = groupItems;
+ collectionItems.items.map((it: any) => {
+ response.itemsAvailable++;
+ response.items.push(it);
+ });
+ processItems.items.map((it: any) => {
+ response.itemsAvailable++;
+ response.items.push(it);
+ });
api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
api.dispatch(resourcesActions.SET_RESOURCES(response.items));
api.dispatch(publicFavoritePanelActions.SET_ITEMS({
- items: response.items.map(resource => resource.uuid),
+ items: response.items.map((resource: any) => resource.uuid),
itemsAvailable: response.itemsAvailable,
page: Math.floor(response.offset / response.limit),
rowsPerPage: response.limit
}));
- api.dispatch<any>(updatePublicFavorites(response.items.map(item => item.headUuid)));
+ api.dispatch<any>(updatePublicFavorites(response.items.map((item: any) => item.uuid)));
} catch (e) {
api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
api.dispatch(publicFavoritePanelActions.SET_ITEMS({
import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
import { ServiceRepository } from "~/services/services";
import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
-import { getResource } from '~/store/resources/resources';
-import { LinkResource } from "~/models/link";
export const publicFavoritesActions = unionize({
TOGGLE_PUBLIC_FAVORITE: ofType<{ resourceUuid: string }>(),
});
};
-export const getHeadUuid = (uuid: string) =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const resource = getResource<LinkResource>(uuid)(getState().resources);
- return resource!.headUuid;
- };
-
export const getIsAdmin = () =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const resource = getState().auth.user!.isAdmin;
import { PUBLIC_FAVORITE_PANEL_ID } from '~/store/public-favorites-panel/public-favorites-action';
import { publicFavoritesReducer } from '~/store/public-favorites/public-favorites-reducer';
import { linkAccountPanelReducer } from './link-account-panel/link-account-panel-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,
} else {
dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash...", kind: SnackbarKind.INFO }));
await services.groupsService.trash(uuid);
+ dispatch(projectPanelActions.REQUEST_ITEMS());
dispatch<any>(loadSidePanelTreeProjects(ownerUuid));
dispatch(snackbarActions.OPEN_SNACKBAR({
message: "Added to trash",
const state = api.getState();
const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
try {
- const response = await this.services.userService.list(getParams(dataExplorer));
- api.dispatch(updateResources(response.items));
- api.dispatch(setItems(response));
+ const responseFirstName = await this.services.userService.list(getParamsFirstName(dataExplorer));
+ if (responseFirstName.itemsAvailable) {
+ api.dispatch(updateResources(responseFirstName.items));
+ api.dispatch(setItems(responseFirstName));
+ } else {
+ const responseLastName = await this.services.userService.list(getParamsLastName(dataExplorer));
+ api.dispatch(updateResources(responseLastName.items));
+ api.dispatch(setItems(responseLastName));
+ }
} catch {
api.dispatch(couldNotFetchUsers());
}
}
}
-export const getParams = (dataExplorer: DataExplorer) => ({
+const getParamsFirstName = (dataExplorer: DataExplorer) => ({
...dataExplorerToListParams(dataExplorer),
order: getOrder(dataExplorer),
- filters: getFilters(dataExplorer)
+ filters: getFiltersFirstName(dataExplorer)
});
-export const getFilters = (dataExplorer: DataExplorer) => {
+const getParamsLastName = (dataExplorer: DataExplorer) => ({
+ ...dataExplorerToListParams(dataExplorer),
+ order: getOrder(dataExplorer),
+ filters: getFiltersLastName(dataExplorer)
+});
+
+const getFiltersFirstName = (dataExplorer: DataExplorer) => {
+ const filters = new FilterBuilder()
+ .addILike("firstName", dataExplorer.searchValue)
+ .getFilters();
+ return filters;
+};
+
+const getFiltersLastName = (dataExplorer: DataExplorer) => {
const filters = new FilterBuilder()
- .addILike("username", dataExplorer.searchValue)
+ .addILike("lastName", dataExplorer.searchValue)
.getFilters();
return filters;
};
import { UserResource } from "~/models/user";
import { getResource } from '~/store/resources/resources';
import { navigateToProject, navigateToUsers, navigateToRootProject } from "~/store/navigation/navigation-action";
-import { saveApiToken, getUserDetails } from '~/store/auth/auth-action';
+import { saveApiToken } from '~/store/auth/auth-action';
export const USERS_PANEL_ID = 'usersPanel';
export const USER_ATTRIBUTES_DIALOG = 'userAttributesDialog';
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 { loadPublicFavoritePanel, publicFavoritePanelActions } from '~/store/public-favorites-panel/public-favorites-action';
import { publicFavoritePanelColumns } from '~/views/public-favorites-panel/public-favorites-panel';
import { USER_LINK_ACCOUNT_KEY } from '~/services/link-account-service/link-account-service';
+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 }));
if (sessionStorage.getItem(USER_LINK_ACCOUNT_KEY)) {
dispatch(linkAccountPanelActions.HAS_SESSION_DATA());
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));
},
});
interface ApiTokenProps {
authService: AuthService;
config: Config;
+ loadMainApp: boolean;
}
export const ApiToken = connect()(
componentDidMount() {
const search = this.props.location ? this.props.location.search : "";
const apiToken = getUrlParameter(search, 'api_token');
+ const loadMainApp = this.props.loadMainApp;
this.props.dispatch(saveApiToken(apiToken));
this.props.dispatch<any>(getUserDetails()).then((user: User) => {
this.props.dispatch(initSessions(this.props.authService, this.props.config, user));
}).finally(() => {
+ if (loadMainApp) {
if (this.props.dispatch(getAccountLinkData())) {
this.props.dispatch(navigateToLinkAccount);
}
else {
this.props.dispatch(navigateToRootProject);
}
+ }
});
}
render() {
- return <div/>;
+ return <div />;
}
}
);
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox, Button } from '@material-ui/core';
-import { FavoriteStar } from '../favorite-star/favorite-star';
+import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox } from '@material-ui/core';
+import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star';
import { ResourceKind, TrashableResource } from '~/models/resource';
import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon, WorkflowIcon, ShareIcon } from '~/components/icon/icon';
import { formatDate, formatFileSize } from '~/common/formatters';
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>
<Grid item>
<Typography variant="caption">
<FavoriteStar resourceUuid={item.uuid} />
+ <PublicFavoriteStar resourceUuid={item.uuid} />
</Typography>
</Grid>
</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) {
return resource || { uuid: '' };
})(renderUuid);
-const renderLinkNameAndIcon = (item: { name: string; headUuid: string, headKind: string }) =>
- <Grid container alignItems="center" wrap="nowrap" spacing={16}>
- <Grid item>
- {renderIcon(item.headKind)}
- </Grid>
- <Grid item>
- <Typography color="primary" style={{ width: 'auto' }}>
- {item.name}
- </Typography>
- </Grid>
- <Grid item>
- <Typography variant="caption">
- <FavoriteStar resourceUuid={item.headUuid} />
- </Typography>
- </Grid>
- </Grid>;
-
-export const ResourceLinkNameAndIcon = connect(
- (state: RootState, props: { uuid: string }) => {
- const resource = getResource<LinkResource>(props.uuid)(state.resources);
- return resource || { name: '', headUuid: '', headKind: '' };
- })(renderLinkNameAndIcon);
-
-export const ResourceLinkType = connect(
- (state: RootState, props: { uuid: string }) => {
- const resource = getResource<LinkResource>(props.uuid)(state.resources);
- return { type: resource ? resource.headKind : '' };
- })((props: { type: string }) => renderType(props.type));
-
// Process Resources
const resourceRunProcess = (dispatch: Dispatch, uuid: string) => {
return (
})((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; isPublicFavoriteVisible: boolean; className?: string; } & WithStyles<CssRules>) => {
+ withStyles(styles)((props: { isFavoriteVisible: boolean; className?: string; } & WithStyles<CssRules>) => {
+ if (props.isFavoriteVisible) {
+ 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} />;
- } else if (props.isFavoriteVisible) {
- return <FavoriteIcon className={props.className || props.classes.icon} />;
+ return <Tooltip enterDelay={500} title="Public Favorite"><PublicFavoriteIcon className={props.className || props.classes.icon} /></Tooltip>;
} else {
return null;
}
({ dispatch }: DispatchProp<any>) =>
<Button
color="inherit"
- onClick={() => dispatch(login("", ""))}>
+ onClick={() => dispatch(login("", "", {}))}>
Sign in
</Button>);
return <div>
<HomeTreePicker pickerId={home} {...p} />
<SharedTreePicker pickerId={shared} {...p} />
- <FavoritesTreePicker pickerId={favorites} {...p} />
<PublicFavoritesTreePicker pickerId={publicFavorites} {...p} />
+ <FavoritesTreePicker pickerId={favorites} {...p} />
</div>;
};
--- /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';
interface FavoritePanelDataProps {
favorites: FavoritesState;
+ isAdmin: boolean;
}
interface FavoritePanelActionProps {
onItemClick: (item: string) => void;
- onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
onDialogOpen: (ownerUuid: string) => void;
onItemDoubleClick: (item: string) => void;
}
-const mapStateToProps = ({ favorites }: RootState): FavoritePanelDataProps => ({
- favorites
-});
-
-const mapDispatchToProps = (dispatch: Dispatch): FavoritePanelActionProps => ({
- onContextMenu: (event, resourceUuid) => {
- const kind = resourceKindToContextMenuKind(resourceUuid);
- if (kind) {
- dispatch<any>(openContextMenu(event, {
- name: '',
- uuid: resourceUuid,
- ownerUuid: '',
- kind: ResourceKind.NONE,
- menuKind: kind
- }));
- }
- dispatch<any>(loadDetailsPanel(resourceUuid));
- },
- onDialogOpen: (ownerUuid: string) => { return; },
- onItemClick: (resourceUuid: string) => {
- dispatch<any>(loadDetailsPanel(resourceUuid));
- },
- onItemDoubleClick: uuid => {
- dispatch<any>(navigateTo(uuid));
- }
+const mapStateToProps = (state : RootState): FavoritePanelDataProps => ({
+ favorites: state.favorites,
+ isAdmin: state.auth.user!.isAdmin
});
type FavoritePanelProps = FavoritePanelDataProps & FavoritePanelActionProps & DispatchProp
& WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
export const FavoritePanel = withStyles(styles)(
- connect(mapStateToProps, mapDispatchToProps)(
+ connect(mapStateToProps)(
class extends React.Component<FavoritePanelProps> {
+
+ handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+ const menuKind = resourceKindToContextMenuKind(resourceUuid, this.props.isAdmin);
+ if (menuKind) {
+ this.props.dispatch<any>(openContextMenu(event, {
+ name: '',
+ uuid: resourceUuid,
+ ownerUuid: '',
+ kind: ResourceKind.NONE,
+ menuKind
+ }));
+ }
+ this.props.dispatch<any>(loadDetailsPanel(resourceUuid));
+ }
+
+ handleRowDoubleClick = (uuid: string) => {
+ this.props.dispatch<any>(navigateTo(uuid));
+ }
+
+ handleRowClick = (uuid: string) => {
+ this.props.dispatch(loadDetailsPanel(uuid));
+ }
+
render() {
return <DataExplorer
id={FAVORITE_PANEL_ID}
- onRowClick={this.props.onItemClick}
- onRowDoubleClick={this.props.onItemDoubleClick}
- onContextMenu={this.props.onContextMenu}
+ onRowClick={this.handleRowClick}
+ onRowDoubleClick={this.handleRowDoubleClick}
+ onContextMenu={this.handleContextMenu}
contextMenuColumn={true}
dataTableDefaultView={
<DataTableDefaultView
<Typography component="div" align="right">
<Button variant="contained" color="primary" style={{ margin: "1em" }} className={classes.button}
- onClick={() => dispatch(login(uuidPrefix, remoteHosts[homeCluster]))}>
+ onClick={() => dispatch(login(uuidPrefix, homeCluster, remoteHosts))}>
Log in to {uuidPrefix}
{uuidPrefix !== homeCluster &&
<span> with user from {homeCluster}</span>}
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
ProcessStatus,
ResourceFileSize,
ResourceLastModifiedDate,
- ResourceLinkNameAndIcon,
- ResourceLinkType
+ ResourceType,
+ ResourceName
} from '~/views-components/data-explorer/renderers';
import { PublicFavoriteIcon } from '~/components/icon/icon';
import { Dispatch } from 'redux';
import { getSimpleObjectTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
import { PUBLIC_FAVORITE_PANEL_ID } from '~/store/public-favorites-panel/public-favorites-action';
import { PublicFavoritesState } from '~/store/public-favorites/public-favorites-reducer';
-import { getHeadUuid, getIsAdmin } from '~/store/public-favorites/public-favorites-actions';
+import { getIsAdmin } from '~/store/public-favorites/public-favorites-actions';
type CssRules = "toolbar" | "button";
},
});
-export enum FavoritePanelColumnNames {
+export enum PublicFavoritePanelColumnNames {
NAME = "Name",
STATUS = "Status",
TYPE = "Type",
export const publicFavoritePanelColumns: DataColumns<string> = [
{
- name: FavoritePanelColumnNames.NAME,
+ name: PublicFavoritePanelColumnNames.NAME,
selected: true,
configurable: true,
sortDirection: SortDirection.NONE,
filters: createTree(),
- render: uuid => <ResourceLinkNameAndIcon uuid={uuid} />
+ render: uuid => <ResourceName uuid={uuid} />
},
{
name: "Status",
render: uuid => <ProcessStatus uuid={uuid} />
},
{
- name: FavoritePanelColumnNames.TYPE,
+ name: PublicFavoritePanelColumnNames.TYPE,
selected: true,
configurable: true,
filters: getSimpleObjectTypeFilters(),
- render: uuid => <ResourceLinkType uuid={uuid} />
+ render: uuid => <ResourceType uuid={uuid} />
},
{
- name: FavoritePanelColumnNames.FILE_SIZE,
+ name: PublicFavoritePanelColumnNames.FILE_SIZE,
selected: true,
configurable: true,
filters: createTree(),
render: uuid => <ResourceFileSize uuid={uuid} />
},
{
- name: FavoritePanelColumnNames.LAST_MODIFIED,
+ name: PublicFavoritePanelColumnNames.LAST_MODIFIED,
selected: true,
configurable: true,
sortDirection: SortDirection.DESC,
const mapDispatchToProps = (dispatch: Dispatch): PublicFavoritePanelActionProps => ({
onContextMenu: (event, resourceUuid) => {
const isAdmin = dispatch<any>(getIsAdmin());
- const kind = resourceKindToContextMenuKind(dispatch<any>(getHeadUuid(resourceUuid)), isAdmin);
+ const kind = resourceKindToContextMenuKind(resourceUuid, isAdmin);
if (kind) {
dispatch<any>(openContextMenu(event, {
name: '',
- uuid: dispatch<any>(getHeadUuid(resourceUuid)),
+ uuid: resourceUuid,
ownerUuid: '',
kind: ResourceKind.NONE,
menuKind: kind
}));
}
- dispatch<any>(loadDetailsPanel(dispatch<any>(getHeadUuid(resourceUuid))));
+ dispatch<any>(loadDetailsPanel(resourceUuid));
},
onDialogOpen: (ownerUuid: string) => { return; },
onItemClick: (uuid: string) => {
- dispatch<any>(loadDetailsPanel(dispatch<any>(getHeadUuid(uuid))));
+ dispatch<any>(loadDetailsPanel(uuid));
},
onItemDoubleClick: uuid => {
- dispatch<any>(navigateTo(dispatch<any>(getHeadUuid(uuid))));
+ dispatch<any>(navigateTo(uuid));
}
});
} from '~/views-components/data-explorer/renderers';
import { createTree } from '~/models/tree';
import { getInitialResourceTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
+import { User } from "~/models/user";
+import { Config } from '~/common/config';
+import { Session } from "~/models/session";
export enum SearchResultsPanelColumnNames {
CLUSTER = "Cluster",
export interface SearchResultsPanelDataProps {
data: SearchBarAdvanceFormData;
+ user: User;
+ sessions: Session[];
+ remoteHostsConfig: { [key: string]: Config };
+ localCluster: string;
}
export interface SearchResultsPanelActionProps {
];
export const SearchResultsPanelView = (props: SearchResultsPanelProps) => {
+ const homeCluster = props.user.uuid.substr(0, 5);
return <DataExplorer
id={SEARCH_RESULTS_PANEL_ID}
onRowClick={props.onItemClick}
onRowDoubleClick={props.onItemDoubleClick}
onContextMenu={props.onContextMenu}
contextMenuColumn={true}
- hideSearchInput />;
+ hideSearchInput
+ title={
+ props.localCluster === homeCluster ?
+ <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 { ResourceKind } from '~/models/resource';
import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
import { SearchResultsPanelView } from '~/views/search-results-panel/search-results-panel-view';
+import { RootState } from '~/store/store';
+
+const mapStateToProps = (rootState: RootState) => {
+ return {
+ user: rootState.auth.user,
+ sessions: rootState.auth.sessions,
+ remoteHostsConfig: rootState.auth.remoteHostsConfig,
+ localCluster: rootState.auth.localCluster,
+ };
+};
const mapDispatchToProps = (dispatch: Dispatch): SearchResultsPanelActionProps => ({
onContextMenu: (event, resourceUuid) => {
}
});
-export const SearchResultsPanel = connect(null, mapDispatchToProps)(SearchResultsPanelView);
\ No newline at end of file
+export const SearchResultsPanel = connect(mapStateToProps, mapDispatchToProps)(SearchResultsPanelView);
interface SharedWithMePanelDataProps {
resources: ResourcesState;
+ isAdmin: boolean;
}
type SharedWithMePanelProps = SharedWithMePanelDataProps & DispatchProp & WithStyles<CssRules>;
export const SharedWithMePanel = withStyles(styles)(
connect((state: RootState) => ({
- resources: state.resources
+ resources: state.resources,
+ isAdmin: state.auth.user!.isAdmin
}))(
class extends React.Component<SharedWithMePanelProps> {
render() {
}
handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
- const menuKind = resourceKindToContextMenuKind(resourceUuid);
+ const menuKind = resourceKindToContextMenuKind(resourceUuid, this.props.isAdmin);
const resource = getResource<GroupResource>(resourceUuid)(this.props.resources);
if (menuKind && resource) {
this.props.dispatch<any>(openContextMenu(event, {
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { connect } from 'react-redux';
+import { RootState } from '~/store/store';
+import { AuthState } from '~/store/auth/auth-reducer';
+import { User } from "~/models/user";
+import { getSaltedToken } from '~/store/auth/auth-action-session';
+import { Config } from '~/common/config';
+
+export interface FedLoginProps {
+ user?: User;
+ apiToken?: string;
+ homeCluster: string;
+ remoteHostsConfig: { [key: string]: Config };
+}
+
+const mapStateToProps = ({ auth }: RootState) => ({
+ user: auth.user,
+ apiToken: auth.apiToken,
+ remoteHostsConfig: auth.remoteHostsConfig,
+ homeCluster: auth.homeCluster,
+});
+
+export const FedLogin = connect(mapStateToProps)(
+ class extends React.Component<FedLoginProps> {
+ render() {
+ const { apiToken, user, homeCluster, remoteHostsConfig } = this.props;
+ if (!apiToken || !user || !user.uuid.startsWith(homeCluster)) {
+ return <></>;
+ }
+ const [, tokenUuid, token] = apiToken.split("/");
+ return <div id={"fedtoken-iframe-div"}>
+ {Object.keys(remoteHostsConfig)
+ .map((k) => k !== homeCluster &&
+ <iframe key={k} src={`${remoteHostsConfig[k].workbench2Url}/fedtoken?api_token=${getSaltedToken(k, tokenUuid, token)}`} style={{
+ height: 0,
+ width: 0,
+ visibility: "hidden"
+ }}
+ />)}
+ </div>;
+ }
+ });
import { PartialCopyToCollectionDialog } from '~/views-components/dialog-forms/partial-copy-to-collection-dialog';
import { PublicFavoritePanel } from '~/views/public-favorites-panel/public-favorites-panel';
import { LinkAccountPanel } from '~/views/link-account-panel/link-account-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.LINKS} component={LinkPanel} />
<Route path={Routes.PUBLIC_FAVORITES} component={PublicFavoritePanel} />
<Route path={Routes.LINK_ACCOUNT} component={LinkAccountPanel} />
+ <Route path={Routes.COLLECTIONS_CONTENT_ADDRESS} component={CollectionsContentAddressPanel} />
</Switch>
</Grid>
</Grid>
<UserAttributesDialog />
<UserManageDialog />
<VirtualMachineAttributesDialog />
+ <FedLogin />
</Grid>
);