From: Eric Biagiotti Date: Wed, 15 May 2019 15:04:57 +0000 (-0400) Subject: Merge branch 'master' into 15088-merge-account X-Git-Tag: 1.4.0~1^2~13 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/3b53b656e65fdabc32b3bc748074eb35e9df98eb?hp=967f2bb4cc911c0f3dae6246ac81e19c177751ba Merge branch 'master' into 15088-merge-account --- diff --git a/src/common/config.ts b/src/common/config.ts index 3961d5aa..71b7774c 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -51,6 +51,7 @@ export interface Config { version: string; websocketUrl: string; workbenchUrl: string; + workbench2Url?: string; vocabularyUrl: string; fileViewersConfigUrl: string; } @@ -136,4 +137,4 @@ const getDefaultConfig = (): ConfigJSON => ({ }); 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()}`; diff --git a/src/components/data-explorer/data-explorer.tsx b/src/components/data-explorer/data-explorer.tsx index 7c1f9045..7107bd70 100644 --- a/src/components/data-explorer/data-explorer.tsx +++ b/src/components/data-explorer/data-explorer.tsx @@ -14,7 +14,7 @@ import { DataTableFilters } from '~/components/data-table-filters/data-table-fil 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 = (theme: ArvadosTheme) => ({ searchBox: { @@ -23,6 +23,9 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ toolbar: { paddingTop: theme.spacing.unit * 2 }, + toolbarUnderTitle: { + paddingTop: 0 + }, footer: { overflow: 'auto' }, @@ -31,6 +34,11 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ }, moreOptionsButton: { padding: 0 + }, + title: { + paddingLeft: theme.spacing.unit * 3, + paddingTop: theme.spacing.unit * 3, + fontSize: '18px' } }); @@ -50,6 +58,7 @@ interface DataExplorerDataProps { paperProps?: PaperProps; actions?: React.ReactNode; hideSearchInput?: boolean; + title?: React.ReactNode; paperKey?: string; currentItemUuid: string; } @@ -84,10 +93,11 @@ export const DataExplorer = withStyles(styles)( 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 - {(!hideColumnSelector || !hideSearchInput) && + {title &&
{title}
} + {(!hideColumnSelector || !hideSearchInput) &&
{!hideSearchInput && void; + linkInsideCard?: string; } type DetailsAttributeProps = DetailsAttributeDataProps & WithStyles; 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) => {label} - { link - ? {value} - : - {value} - {children} - } + {link && {value}} + {linkInsideCard && {value}} + {!link && !linkInsideCard && + {value} + {children} + + } + ); diff --git a/src/index.tsx b/src/index.tsx index 9f9b27ca..ee174b2c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -14,7 +14,6 @@ import { configureStore, RootStore } from '~/store/store'; 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'; @@ -40,7 +39,6 @@ import { addRouteChangeHandlers } from './routes/route-change-handlers'; 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'; @@ -115,7 +113,8 @@ fetchConfig() store.dispatch(loadVocabulary); store.dispatch(loadFileViewersConfig); - const TokenComponent = (props: any) => ; + const TokenComponent = (props: any) => ; + const FedTokenComponent = (props: any) => ; const MainPanelComponent = (props: any) => ; const App = () => @@ -125,6 +124,7 @@ fetchConfig() + diff --git a/src/routes/route-change-handlers.ts b/src/routes/route-change-handlers.ts index e0a51550..b43e84bb 100644 --- a/src/routes/route-change-handlers.ts +++ b/src/routes/route-change-handlers.ts @@ -23,7 +23,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => { 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); @@ -46,6 +46,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => { 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()); @@ -105,5 +106,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => { 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 diff --git a/src/routes/routes.ts b/src/routes/routes.ts index ba7e2a45..76f5c32d 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -10,6 +10,7 @@ import { getCollectionUrl } from '~/models/collection'; 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})`, @@ -35,7 +36,8 @@ export const Routes = { 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) => { @@ -143,5 +145,8 @@ export const matchGroupDetailsRoute = (route: 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 }); diff --git a/src/services/auth-service/auth-service.ts b/src/services/auth-service/auth-service.ts index eae219dd..a80d89ba 100644 --- a/src/services/auth-service/auth-service.ts +++ b/src/services/auth-service/auth-service.ts @@ -20,6 +20,7 @@ export const USER_IS_ADMIN = 'isAdmin'; 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; @@ -42,6 +43,7 @@ export class AuthService { public saveApiToken(token: string) { localStorage.setItem(API_TOKEN_KEY, token); + localStorage.setItem(HOME_CLUSTER, token.split('/')[1].substr(0, 5)); } public removeApiToken() { @@ -52,6 +54,10 @@ export class AuthService { return localStorage.getItem(API_TOKEN_KEY) || undefined; } + public getHomeCluster() { + return localStorage.getItem(HOME_CLUSTER) || undefined; + } + public getUuid() { return localStorage.getItem(USER_UUID_KEY) || undefined; } @@ -108,9 +114,10 @@ export class AuthService { 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() { diff --git a/src/store/auth/auth-action-session.ts b/src/store/auth/auth-action-session.ts index 5bb192b8..b889e9cf 100644 --- a/src/store/auth/auth-action-session.ts +++ b/src/store/auth/auth-action-session.ts @@ -68,7 +68,7 @@ const getTokenUuid = async (baseUrl: string, token: string): Promise => 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/")) { diff --git a/src/store/auth/auth-action.ts b/src/store/auth/auth-action.ts index 7ee83992..6ca71403 100644 --- a/src/store/auth/auth-action.ts +++ b/src/store/auth/auth-action.ts @@ -10,10 +10,12 @@ import { ServiceRepository } from "~/services/services"; 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(), @@ -31,7 +33,8 @@ export const authActions = unionize({ SET_SESSIONS: ofType(), ADD_SESSION: ofType(), REMOVE_SESSION: ofType(), - UPDATE_SESSION: ofType() + UPDATE_SESSION: ofType(), + REMOTE_CLUSTER_CONFIG: ofType<{ config: Config }>(), }); function setAuthorizationHeader(services: ServiceRepository, token: string) { @@ -57,17 +60,30 @@ export const initAuth = (config: Config) => (dispatch: Dispatch, getState: () => 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(initSessions(services.authService, config, user)); dispatch(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(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) => { @@ -81,8 +97,8 @@ export const saveUser = (user: UserResource) => (dispatch: Dispatch, getState: ( 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()); }; diff --git a/src/store/auth/auth-reducer.ts b/src/store/auth/auth-reducer.ts index c87fc5dd..cded9f0e 100644 --- a/src/store/auth/auth-reducer.ts +++ b/src/store/auth/auth-reducer.ts @@ -7,6 +7,7 @@ import { User, UserResource } from "~/models/user"; 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; @@ -16,6 +17,7 @@ export interface AuthState { localCluster: string; homeCluster: string; remoteHosts: { [key: string]: string }; + remoteHostsConfig: { [key: string]: Config }; } const initialState: AuthState = { @@ -25,7 +27,8 @@ const initialState: AuthState = { sessions: [], localCluster: "", homeCluster: "", - remoteHosts: {} + remoteHosts: {}, + remoteHostsConfig: {} }; export const authReducer = (services: ServiceRepository) => (state = initialState, action: AuthAction) => { @@ -44,6 +47,12 @@ export const authReducer = (services: ServiceRepository) => (state = initialStat 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) }; }, diff --git a/src/store/collection-panel/collection-panel-action.ts b/src/store/collection-panel/collection-panel-action.ts index cd426471..b1dd8389 100644 --- a/src/store/collection-panel/collection-panel-action.ts +++ b/src/store/collection-panel/collection-panel-action.ts @@ -7,7 +7,7 @@ import { loadCollectionFiles } from "./collection-panel-files/collection-panel-f 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"; @@ -15,6 +15,7 @@ import { resourcesActions } from "~/store/resources/resources-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(), @@ -31,6 +32,7 @@ export const loadCollectionPanel = (uuid: string) => 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(loadCollectionFiles(collection.uuid)); diff --git a/src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts b/src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts index b75de94a..534d70d4 100644 --- a/src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts +++ b/src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts @@ -77,8 +77,8 @@ export const openFileRemoveDialog = (filePath: string) => ? '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, @@ -101,7 +101,7 @@ export const openMultipleFilesRemoveDialog = () => 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' } }); diff --git a/src/store/collection-panel/collection-panel-reducer.ts b/src/store/collection-panel/collection-panel-reducer.ts index 55829cb5..f09b0198 100644 --- a/src/store/collection-panel/collection-panel-reducer.ts +++ b/src/store/collection-panel/collection-panel-reducer.ts @@ -17,5 +17,5 @@ export const collectionPanelReducer = (state: CollectionPanelState = initialStat collectionPanelActions.match(action, { default: () => state, SET_COLLECTION: (item) => ({ ...state, item }), - LOAD_COLLECTION_SUCCESS: ({ item }) => ({ ...state, item }), + LOAD_COLLECTION_SUCCESS: ({ item }) => ({ ...state, item }) }); diff --git a/src/store/collections-content-address-panel/collections-content-address-middleware-service.ts b/src/store/collections-content-address-panel/collections-content-address-middleware-service.ts new file mode 100644 index 00000000..642e7b82 --- /dev/null +++ b/src/store/collections-content-address-panel/collections-content-address-middleware-service.ts @@ -0,0 +1,137 @@ +// 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) { + const dataExplorer = getDataExplorer(api.getState().dataExplorer, this.getId()); + if (!dataExplorer) { + api.dispatch(collectionPanelDataExplorerIsNotSet()); + } else { + const sortColumn = getSortColumn(dataExplorer); + + const contentOrder = new OrderBuilder(); + + 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(ownerNameActions.SET_OWNER_NAME({name: it.uuid === userUuid ? 'User: Me' : `User: ${it.firstName} ${it.lastName}`, uuid: it.uuid})); + }); + responseGroups.items.map(it=>{ + api.dispatch(ownerNameActions.SET_OWNER_NAME({name: `Project: ${it.name}`, uuid: it.uuid})); + }); + api.dispatch(setBreadcrumbs([{ label: 'Projects', uuid: userUuid }])); + api.dispatch(updateFavorites(response.items.map(item => item.uuid))); + api.dispatch(updatePublicFavorites(response.items.map(item => item.uuid))); + if (response.itemsAvailable === 1) { + api.dispatch(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 diff --git a/src/store/collections-content-address-panel/collections-content-address-panel-actions.ts b/src/store/collections-content-address-panel/collections-content-address-panel-actions.ts new file mode 100644 index 00000000..11f1a8cc --- /dev/null +++ b/src/store/collections-content-address-panel/collections-content-address-panel-actions.ts @@ -0,0 +1,15 @@ +// 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()); + }; diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts index b21ca0a7..431d15e8 100644 --- a/src/store/context-menu/context-menu-actions.ts +++ b/src/store/context-menu/context-menu-actions.ts @@ -152,12 +152,13 @@ export const openRootProjectContextMenu = (event: React.MouseEvent, export const openProjectContextMenu = (event: React.MouseEvent, projectUuid: string) => (dispatch: Dispatch, getState: () => RootState) => { const res = getResource(projectUuid)(getState().resources); + const isAdmin = getState().auth.user!.isAdmin; if (res) { dispatch(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 })); diff --git a/src/store/favorite-panel/favorite-panel-middleware-service.ts b/src/store/favorite-panel/favorite-panel-middleware-service.ts index e518482b..868d7b05 100644 --- a/src/store/favorite-panel/favorite-panel-middleware-service.ts +++ b/src/store/favorite-panel/favorite-panel-middleware-service.ts @@ -56,14 +56,14 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic } 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) @@ -94,7 +94,7 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic response.itemsAvailable++; response.items.push(it); }); - + api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); api.dispatch(resourcesActions.SET_RESOURCES(response.items)); await api.dispatch(loadMissingProcessesInformation(response.items)); diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts index 3bec1609..6d393f03 100644 --- a/src/store/navigation/navigation-action.ts +++ b/src/store/navigation/navigation-action.ts @@ -100,3 +100,5 @@ export const navigateToGroups = push(Routes.GROUPS); export const navigateToGroupDetails = compose(push, getGroupUrl); export const navigateToLinks = push(Routes.LINKS); + +export const navigateToCollectionsContentAddress = push(Routes.COLLECTIONS_CONTENT_ADDRESS); diff --git a/src/store/owner-name/owner-name-actions.ts b/src/store/owner-name/owner-name-actions.ts new file mode 100644 index 00000000..6c2784a1 --- /dev/null +++ b/src/store/owner-name/owner-name-actions.ts @@ -0,0 +1,16 @@ +// 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() +}); + +interface OwnerNameState { + name: string; + uuid: string; +} + +export type OwnerNameAction = UnionOf; diff --git a/src/store/owner-name/owner-name-reducer.ts b/src/store/owner-name/owner-name-reducer.ts new file mode 100644 index 00000000..58df209f --- /dev/null +++ b/src/store/owner-name/owner-name-reducer.ts @@ -0,0 +1,11 @@ +// 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 diff --git a/src/store/projects/project-move-actions.ts b/src/store/projects/project-move-actions.ts index 365e07aa..8876be0f 100644 --- a/src/store/projects/project-move-actions.ts +++ b/src/store/projects/project-move-actions.ts @@ -11,6 +11,8 @@ import { getCommonResourceServiceError, CommonResourceServiceError } from "~/ser 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'; @@ -24,11 +26,14 @@ export const openMoveProjectDialog = (resource: { name: string, uuid: string }) 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(loadSidePanelTreeProjects(userUuid)); return newProject; } catch (e) { const error = getCommonResourceServiceError(e); diff --git a/src/store/projects/project-update-actions.ts b/src/store/projects/project-update-actions.ts index 321b8554..b9206976 100644 --- a/src/store/projects/project-update-actions.ts +++ b/src/store/projects/project-update-actions.ts @@ -11,6 +11,7 @@ import { ServiceRepository } from "~/services/services"; 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; @@ -33,6 +34,7 @@ export const updateProject = (project: Partial) => 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) { diff --git a/src/store/public-favorites-panel/public-favorites-middleware-service.ts b/src/store/public-favorites-panel/public-favorites-middleware-service.ts index c7bbf8dd..be7f5285 100644 --- a/src/store/public-favorites-panel/public-favorites-middleware-service.ts +++ b/src/store/public-favorites-panel/public-favorites-middleware-service.ts @@ -20,7 +20,6 @@ import { serializeSimpleObjectTypeFilters } from '../resource-type-filters/resou 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 { @@ -56,7 +55,7 @@ export class PublicFavoritesMiddlewareService extends DataExplorerMiddlewareServ 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() @@ -66,15 +65,46 @@ export class PublicFavoritesMiddlewareService extends DataExplorerMiddlewareServ .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(updatePublicFavorites(response.items.map(item => item.headUuid))); + api.dispatch(updatePublicFavorites(response.items.map((item: any) => item.uuid))); } catch (e) { api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); api.dispatch(publicFavoritePanelActions.SET_ITEMS({ diff --git a/src/store/public-favorites/public-favorites-actions.ts b/src/store/public-favorites/public-favorites-actions.ts index b5aa9fce..50b9070b 100644 --- a/src/store/public-favorites/public-favorites-actions.ts +++ b/src/store/public-favorites/public-favorites-actions.ts @@ -9,8 +9,6 @@ import { checkPublicFavorite } from "./public-favorites-reducer"; 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 }>(), @@ -69,12 +67,6 @@ export const updatePublicFavorites = (resourceUuids: string[]) => }); }; -export const getHeadUuid = (uuid: string) => - (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - const resource = getResource(uuid)(getState().resources); - return resource!.headUuid; - }; - export const getIsAdmin = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { const resource = getState().auth.user!.isAdmin; diff --git a/src/store/store.ts b/src/store/store.ts index 6a37572b..8a2ca240 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -63,6 +63,9 @@ import { PublicFavoritesMiddlewareService } from '~/store/public-favorites-panel 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' && @@ -116,6 +119,10 @@ export function configureStore(history: History, services: ServiceRepository): R 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), @@ -131,7 +138,8 @@ export function configureStore(history: History, services: ServiceRepository): R linkPanelMiddleware, computeNodeMiddleware, apiClientAuthorizationMiddlewareService, - publicFavoritesMiddleware + publicFavoritesMiddleware, + collectionsContentAddress ]; const enhancer = composeEnhancers(applyMiddleware(...middlewares)); return createStore(rootReducer, enhancer); @@ -147,6 +155,7 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({ detailsPanel: detailsPanelReducer, dialog: dialogReducer, favorites: favoritesReducer, + ownerName: ownerNameReducer, publicFavorites: publicFavoritesReducer, form: formReducer, processLogsPanel: processLogsPanelReducer, diff --git a/src/store/trash/trash-actions.ts b/src/store/trash/trash-actions.ts index 693a0ece..b810b1e4 100644 --- a/src/store/trash/trash-actions.ts +++ b/src/store/trash/trash-actions.ts @@ -29,6 +29,7 @@ export const toggleProjectTrashed = (uuid: string, ownerUuid: string, isTrashed: } else { dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash...", kind: SnackbarKind.INFO })); await services.groupsService.trash(uuid); + dispatch(projectPanelActions.REQUEST_ITEMS()); dispatch(loadSidePanelTreeProjects(ownerUuid)); dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Added to trash", diff --git a/src/store/users/user-panel-middleware-service.ts b/src/store/users/user-panel-middleware-service.ts index bc4bb130..cffcce4d 100644 --- a/src/store/users/user-panel-middleware-service.ts +++ b/src/store/users/user-panel-middleware-service.ts @@ -27,24 +27,43 @@ export class UserMiddlewareService extends DataExplorerMiddlewareService { 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; }; diff --git a/src/store/users/users-actions.ts b/src/store/users/users-actions.ts index 066aa80b..caf466f7 100644 --- a/src/store/users/users-actions.ts +++ b/src/store/users/users-actions.ts @@ -12,7 +12,7 @@ import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions 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'; diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts index 0cffcace..27ac76f3 100644 --- a/src/store/workbench/workbench-actions.ts +++ b/src/store/workbench/workbench-actions.ts @@ -69,8 +69,7 @@ import { FilterBuilder } from '~/services/api/filter-builder'; 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, @@ -96,6 +95,8 @@ 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 { 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'; @@ -135,6 +136,7 @@ export const loadWorkbench = () => 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()); @@ -160,6 +162,11 @@ export const loadFavorites = () => dispatch(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES)); }); +export const loadCollectionContentAddress = handleFirstTimeLoad( + async (dispatch: Dispatch) => { + await dispatch(loadCollectionsContentAddressPanel()); + }); + export const loadTrash = () => handleFirstTimeLoad( (dispatch: Dispatch) => { @@ -263,21 +270,21 @@ export const loadCollection = (uuid: string) => 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(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)); }, }); diff --git a/src/views-components/api-token/api-token.tsx b/src/views-components/api-token/api-token.tsx index b0fd0313..2f8d87e1 100644 --- a/src/views-components/api-token/api-token.tsx +++ b/src/views-components/api-token/api-token.tsx @@ -17,6 +17,7 @@ import { getAccountLinkData } from "~/store/link-account-panel/link-account-pane interface ApiTokenProps { authService: AuthService; config: Config; + loadMainApp: boolean; } export const ApiToken = connect()( @@ -24,20 +25,23 @@ 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(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
; + return
; } } ); diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx index 04dbfa7c..bf504f2b 100644 --- a/src/views-components/data-explorer/renderers.tsx +++ b/src/views-components/data-explorer/renderers.tsx @@ -3,8 +3,8 @@ // 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'; @@ -27,19 +27,20 @@ import { LinkResource } from '~/models/link'; 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 }) => {renderIcon(item.kind)} - + dispatch(navigateTo(item.uuid))}> {item.name} + ; @@ -48,7 +49,7 @@ export const ResourceName = connect( (state: RootState, props: { uuid: string }) => { const resource = getResource(props.uuid)(state.resources); return resource || { name: '', uuid: '', kind: '' }; - })(renderName); + })((resource: { name: string; uuid: string, kind: string } & DispatchProp) => renderName(resource.dispatch, resource)); const renderIcon = (kind: string) => { switch (kind) { @@ -326,35 +327,6 @@ export const ResourceLinkUuid = connect( return resource || { uuid: '' }; })(renderUuid); -const renderLinkNameAndIcon = (item: { name: string; headUuid: string, headKind: string }) => - - - {renderIcon(item.headKind)} - - - - {item.name} - - - - - - - - ; - -export const ResourceLinkNameAndIcon = connect( - (state: RootState, props: { uuid: string }) => { - const resource = getResource(props.uuid)(state.resources); - return resource || { name: '', headUuid: '', headKind: '' }; - })(renderLinkNameAndIcon); - -export const ResourceLinkType = connect( - (state: RootState, props: { uuid: string }) => { - const resource = getResource(props.uuid)(state.resources); - return { type: resource ? resource.headKind : '' }; - })((props: { type: string }) => renderType(props.type)); - // Process Resources const resourceRunProcess = (dispatch: Dispatch, uuid: string) => { return ( @@ -429,7 +401,7 @@ export const ResourceFileSize = connect( })((props: { fileSize?: number }) => renderFileSize(props.fileSize)); const renderOwner = (owner: string) => - + {owner} ; @@ -439,6 +411,14 @@ export const ResourceOwner = connect( return { owner: resource ? resource.ownerUuid : '' }; })((props: { owner: string }) => renderOwner(props.owner)); +export const ResourceOwnerName = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(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) => {resourceLabel(type)} diff --git a/src/views-components/details-panel/collection-details.tsx b/src/views-components/details-panel/collection-details.tsx index 98fa3886..ec5bdabd 100644 --- a/src/views-components/details-panel/collection-details.tsx +++ b/src/views-components/details-panel/collection-details.tsx @@ -24,9 +24,8 @@ export class CollectionDetails extends DetailsData { - {/* Links but we dont have view */} - - + + {/* Missing attrs */} diff --git a/src/views-components/details-panel/details-panel.tsx b/src/views-components/details-panel/details-panel.tsx index 2a30ae47..9ce84867 100644 --- a/src/views-components/details-panel/details-panel.tsx +++ b/src/views-components/details-panel/details-panel.tsx @@ -85,7 +85,7 @@ const mapStateToProps = ({ detailsPanel, resources, resourcesData, collectionPan 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), }; }; diff --git a/src/views-components/favorite-star/favorite-star.tsx b/src/views-components/favorite-star/favorite-star.tsx index cf744886..0598e5f5 100644 --- a/src/views-components/favorite-star/favorite-star.tsx +++ b/src/views-components/favorite-star/favorite-star.tsx @@ -6,7 +6,7 @@ import * as React from "react"; 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"; @@ -23,11 +23,18 @@ const mapStateToProps = (state: RootState, props: { resourceUuid: string; classN }); export const FavoriteStar = connect(mapStateToProps)( - withStyles(styles)((props: { isFavoriteVisible: boolean; isPublicFavoriteVisible: boolean; className?: string; } & WithStyles) => { + withStyles(styles)((props: { isFavoriteVisible: boolean; className?: string; } & WithStyles) => { + if (props.isFavoriteVisible) { + return ; + } else { + return null; + } + })); + +export const PublicFavoriteStar = connect(mapStateToProps)( + withStyles(styles)((props: { isPublicFavoriteVisible: boolean; className?: string; } & WithStyles) => { if (props.isPublicFavoriteVisible) { - return ; - } else if (props.isFavoriteVisible) { - return ; + return ; } else { return null; } diff --git a/src/views-components/main-app-bar/anonymous-menu.tsx b/src/views-components/main-app-bar/anonymous-menu.tsx index 5ffc4215..15329a43 100644 --- a/src/views-components/main-app-bar/anonymous-menu.tsx +++ b/src/views-components/main-app-bar/anonymous-menu.tsx @@ -11,6 +11,6 @@ export const AnonymousMenu = connect()( ({ dispatch }: DispatchProp) => ); diff --git a/src/views-components/projects-tree-picker/projects-tree-picker.tsx b/src/views-components/projects-tree-picker/projects-tree-picker.tsx index 653ae604..f4969f5c 100644 --- a/src/views-components/projects-tree-picker/projects-tree-picker.tsx +++ b/src/views-components/projects-tree-picker/projects-tree-picker.tsx @@ -32,8 +32,8 @@ export const ProjectsTreePicker = ({ pickerId, ...props }: ProjectsTreePickerPro return
- +
; }; diff --git a/src/views/collection-content-address-panel/collection-content-address-panel.tsx b/src/views/collection-content-address-panel/collection-content-address-panel.tsx new file mode 100644 index 00000000..b652b502 --- /dev/null +++ b/src/views/collection-content-address-panel/collection-content-address-panel.tsx @@ -0,0 +1,154 @@ +// 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 = (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 = [ + { + name: CollectionContentAddressPanelColumnNames.COLLECTION_WITH_THIS_ADDRESS, + selected: true, + configurable: true, + sortDirection: SortDirection.NONE, + filters: createTree(), + render: uuid => + }, + { + name: CollectionContentAddressPanelColumnNames.LOCATION, + selected: true, + configurable: true, + filters: createTree(), + render: uuid => + }, + { + name: CollectionContentAddressPanelColumnNames.LAST_MODIFIED, + selected: true, + configurable: true, + sortDirection: SortDirection.DESC, + filters: createTree(), + render: uuid => + } +]; + +export interface CollectionContentAddressPanelActionProps { + onContextMenu: (event: React.MouseEvent, uuid: string) => void; + onItemClick: (item: string) => void; + onItemDoubleClick: (item: string) => void; +} + +const mapDispatchToProps = (dispatch: Dispatch): CollectionContentAddressPanelActionProps => ({ + onContextMenu: (event, resourceUuid) => { + const isAdmin = dispatch(getIsAdmin()); + const kind = resourceKindToContextMenuKind(resourceUuid, isAdmin); + if (kind) { + dispatch(openContextMenu(event, { + name: '', + uuid: resourceUuid, + ownerUuid: '', + kind: ResourceKind.NONE, + menuKind: kind + })); + } + dispatch(loadDetailsPanel(resourceUuid)); + }, + onItemClick: (uuid: string) => { + dispatch(loadDetailsPanel(uuid)); + }, + onItemDoubleClick: uuid => { + dispatch(navigateTo(uuid)); + } +}); + +interface CollectionContentAddressDataProps { + match: { + params: { id: string } + }; +} + +export const CollectionsContentAddressPanel = withStyles(styles)( + connect(null, mapDispatchToProps)( + class extends React.Component> { + render() { + return + + + } />; + ; + } + } + ) +); diff --git a/src/views/favorite-panel/favorite-panel.tsx b/src/views/favorite-panel/favorite-panel.tsx index 5c19c30b..e7234009 100644 --- a/src/views/favorite-panel/favorite-panel.tsx +++ b/src/views/favorite-panel/favorite-panel.tsx @@ -22,7 +22,6 @@ import { 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'; @@ -107,53 +106,54 @@ export const favoritePanelColumns: DataColumns = [ interface FavoritePanelDataProps { favorites: FavoritesState; + isAdmin: boolean; } interface FavoritePanelActionProps { onItemClick: (item: string) => void; - onContextMenu: (event: React.MouseEvent, 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(openContextMenu(event, { - name: '', - uuid: resourceUuid, - ownerUuid: '', - kind: ResourceKind.NONE, - menuKind: kind - })); - } - dispatch(loadDetailsPanel(resourceUuid)); - }, - onDialogOpen: (ownerUuid: string) => { return; }, - onItemClick: (resourceUuid: string) => { - dispatch(loadDetailsPanel(resourceUuid)); - }, - onItemDoubleClick: uuid => { - dispatch(navigateTo(uuid)); - } +const mapStateToProps = (state : RootState): FavoritePanelDataProps => ({ + favorites: state.favorites, + isAdmin: state.auth.user!.isAdmin }); type FavoritePanelProps = FavoritePanelDataProps & FavoritePanelActionProps & DispatchProp & WithStyles & RouteComponentProps<{ id: string }>; export const FavoritePanel = withStyles(styles)( - connect(mapStateToProps, mapDispatchToProps)( + connect(mapStateToProps)( class extends React.Component { + + handleContextMenu = (event: React.MouseEvent, resourceUuid: string) => { + const menuKind = resourceKindToContextMenuKind(resourceUuid, this.props.isAdmin); + if (menuKind) { + this.props.dispatch(openContextMenu(event, { + name: '', + uuid: resourceUuid, + ownerUuid: '', + kind: ResourceKind.NONE, + menuKind + })); + } + this.props.dispatch(loadDetailsPanel(resourceUuid)); + } + + handleRowDoubleClick = (uuid: string) => { + this.props.dispatch(navigateTo(uuid)); + } + + handleRowClick = (uuid: string) => { + this.props.dispatch(loadDetailsPanel(uuid)); + } + render() { return