From 090f4825bdd30925a10c6df1b9493df0c2e8f541 Mon Sep 17 00:00:00 2001 From: Janicki Artur Date: Wed, 12 Dec 2018 10:48:23 +0100 Subject: [PATCH] add admin links feature - model, service, dialogs and panel Feature #14512_admin_links Arvados-DCO-1.1-Signed-off-by: Janicki Artur --- src/common/labels.ts | 4 + src/index.tsx | 2 + src/models/link.ts | 3 + src/models/resource.ts | 4 + src/routes/route-change-handlers.ts | 5 +- src/routes/routes.ts | 6 +- src/store/advanced-tab/advanced-tab.ts | 50 +++++++++- .../context-menu/context-menu-actions.ts | 2 + src/store/link-panel/link-panel-actions.ts | 57 +++++++++++ .../link-panel-middleware-service.ts | 70 ++++++++++++++ src/store/navigation/navigation-action.ts | 2 + src/store/store.ts | 9 +- src/store/workbench/workbench-actions.ts | 8 ++ .../action-sets/link-action-set.ts | 28 ++++++ .../context-menu/context-menu.tsx | 1 + .../data-explorer/renderers.tsx | 60 +++++++++++- .../links-dialog/attributes-dialog.tsx | 73 ++++++++++++++ .../links-dialog/remove-dialog.tsx | 20 ++++ .../main-app-bar/account-menu.tsx | 19 ++-- .../main-content-bar/main-content-bar.tsx | 3 +- src/views/link-panel/link-panel-root.tsx | 96 +++++++++++++++++++ src/views/link-panel/link-panel.tsx | 35 +++++++ src/views/workbench/workbench.tsx | 6 ++ 23 files changed, 543 insertions(+), 20 deletions(-) create mode 100644 src/store/link-panel/link-panel-actions.ts create mode 100644 src/store/link-panel/link-panel-middleware-service.ts create mode 100644 src/views-components/context-menu/action-sets/link-action-set.ts create mode 100644 src/views-components/links-dialog/attributes-dialog.tsx create mode 100644 src/views-components/links-dialog/remove-dialog.tsx create mode 100644 src/views/link-panel/link-panel-root.tsx create mode 100644 src/views/link-panel/link-panel.tsx diff --git a/src/common/labels.ts b/src/common/labels.ts index 0e3131db..133a0e45 100644 --- a/src/common/labels.ts +++ b/src/common/labels.ts @@ -12,6 +12,10 @@ export const resourceLabel = (type: string) => { return "Project"; case ResourceKind.PROCESS: return "Process"; + case ResourceKind.USER: + return "User"; + case ResourceKind.GROUP: + return "Group"; default: return "Unknown"; } diff --git a/src/index.tsx b/src/index.tsx index 8f702af1..e73f08c4 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -56,6 +56,7 @@ import { virtualMachineActionSet } from '~/views-components/context-menu/action- import { userActionSet } from '~/views-components/context-menu/action-sets/user-action-set'; import { computeNodeActionSet } from '~/views-components/context-menu/action-sets/compute-node-action-set'; import { apiClientAuthorizationActionSet } from '~/views-components/context-menu/action-sets/api-client-authorization-action-set'; +import { linkActionSet } from '~/views-components/context-menu/action-sets/link-action-set'; console.log(`Starting arvados [${getBuildInfo()}]`); @@ -77,6 +78,7 @@ addMenuActionSet(ContextMenuKind.SSH_KEY, sshKeyActionSet); addMenuActionSet(ContextMenuKind.VIRTUAL_MACHINE, virtualMachineActionSet); addMenuActionSet(ContextMenuKind.KEEP_SERVICE, keepServiceActionSet); addMenuActionSet(ContextMenuKind.USER, userActionSet); +addMenuActionSet(ContextMenuKind.LINK, linkActionSet); addMenuActionSet(ContextMenuKind.NODE, computeNodeActionSet); addMenuActionSet(ContextMenuKind.API_CLIENT_AUTHORIZATION, apiClientAuthorizationActionSet); diff --git a/src/models/link.ts b/src/models/link.ts index baaff658..acaf1395 100644 --- a/src/models/link.ts +++ b/src/models/link.ts @@ -4,10 +4,13 @@ import { Resource } from "./resource"; import { TagProperty } from "~/models/tag"; +import { ResourceKind } from '~/models/resource'; export interface LinkResource extends Resource { headUuid: string; + headKind: ResourceKind; tailUuid: string; + tailKind: string; linkClass: string; name: string; properties: TagProperty; diff --git a/src/models/resource.ts b/src/models/resource.ts index eddcd5a0..31f3eb88 100644 --- a/src/models/resource.ts +++ b/src/models/resource.ts @@ -26,6 +26,7 @@ export enum ResourceKind { CONTAINER = "arvados#container", CONTAINER_REQUEST = "arvados#containerRequest", GROUP = "arvados#group", + LINK = "arvados#link", LOG = "arvados#log", NODE = "arvados#node", PROCESS = "arvados#containerRequest", @@ -45,6 +46,7 @@ export enum ResourceObjectType { CONTAINER = 'dz642', CONTAINER_REQUEST = 'xvhdp', GROUP = 'j7d0g', + LINK = 'o0j2j', LOG = '57u5n', REPOSITORY = 's0uqq', USER = 'tpzed', @@ -97,6 +99,8 @@ export const extractUuidKind = (uuid: string = '') => { return ResourceKind.NODE; case ResourceObjectType.API_CLIENT_AUTHORIZATION: return ResourceKind.API_CLIENT_AUTHORIZATION; + case ResourceObjectType.LINK: + return ResourceKind.LINK; default: return undefined; } diff --git a/src/routes/route-change-handlers.ts b/src/routes/route-change-handlers.ts index e2454d63..34b488de 100644 --- a/src/routes/route-change-handlers.ts +++ b/src/routes/route-change-handlers.ts @@ -34,6 +34,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => { const apiClientAuthorizationsMatch = Routes.matchApiClientAuthorizationsRoute(pathname); const myAccountMatch = Routes.matchMyAccountRoute(pathname); const userMatch = Routes.matchUsersRoute(pathname); + const linksMatch = Routes.matchLinksRoute(pathname); if (projectMatch) { store.dispatch(WorkbenchActions.loadProject(projectMatch.params.id)); @@ -71,7 +72,9 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => { store.dispatch(WorkbenchActions.loadApiClientAuthorizations); } else if (myAccountMatch) { store.dispatch(WorkbenchActions.loadMyAccount); - }else if (userMatch) { + } else if (userMatch) { store.dispatch(WorkbenchActions.loadUsers); + } else if (linksMatch) { + store.dispatch(WorkbenchActions.loadLinks); } }; diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 88dfd469..8286e10e 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -27,7 +27,8 @@ export const Routes = { KEEP_SERVICES: `/keep-services`, COMPUTE_NODES: `/nodes`, USERS: '/users', - API_CLIENT_AUTHORIZATIONS: `/api_client_authorizations` + API_CLIENT_AUTHORIZATIONS: `/api_client_authorizations`, + LINKS: '/links' }; export const getResourceUrl = (uuid: string) => { @@ -108,3 +109,6 @@ export const matchComputeNodesRoute = (route: string) => export const matchApiClientAuthorizationsRoute = (route: string) => matchPath(route, { path: Routes.API_CLIENT_AUTHORIZATIONS }); + +export const matchLinksRoute = (route: string) => + matchPath(route, { path: Routes.LINKS }); \ No newline at end of file diff --git a/src/store/advanced-tab/advanced-tab.ts b/src/store/advanced-tab/advanced-tab.ts index 851eb949..659b6e49 100644 --- a/src/store/advanced-tab/advanced-tab.ts +++ b/src/store/advanced-tab/advanced-tab.ts @@ -77,7 +77,8 @@ enum ResourcePrefix { KEEP_SERVICES = 'keep_services', COMPUTE_NODES = 'nodes', USERS = 'users', - API_CLIENT_AUTHORIZATIONS = 'api_client_authorizations' + API_CLIENT_AUTHORIZATIONS = 'api_client_authorizations', + LINKS = 'links' } enum KeepServiceData { @@ -100,9 +101,14 @@ enum ApiClientAuthorizationsData { DEFAULT_OWNER_UUID = 'default_owner_uuid' } -type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData | VirtualMachineData | KeepServiceData | ComputeNodeData | ApiClientAuthorizationsData | UserData; +enum LinkData { + LINK = 'link', + PROPERTIES = 'properties' +} + +type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData | VirtualMachineData | KeepServiceData | ComputeNodeData | ApiClientAuthorizationsData | UserData | LinkData; type AdvanceResourcePrefix = GroupContentsResourcePrefix | ResourcePrefix; -type AdvanceResponseData = ContainerRequestResource | ProjectResource | CollectionResource | RepositoryResource | SshKeyResource | VirtualMachinesResource | KeepServiceResource | NodeResource | ApiClientAuthorization | UserResource | undefined; +type AdvanceResponseData = ContainerRequestResource | ProjectResource | CollectionResource | RepositoryResource | SshKeyResource | VirtualMachinesResource | KeepServiceResource | NodeResource | ApiClientAuthorization | UserResource | LinkResource | undefined; export const openAdvancedTabDialog = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { @@ -264,6 +270,22 @@ export const openAdvancedTabDialog = (uuid: string) => }); dispatch(initAdvancedTabDialog(advanceDataApiClientAuthorization)); break; + case ResourceKind.LINK: + const linkResources = getState().resources; + const dataLink = getResource(uuid)(linkResources); + const advanceDataLink = advancedTabData({ + uuid, + metadata: '', + user: '', + apiResponseKind: linkApiResponse, + data: dataLink, + resourceKind: LinkData.LINK, + resourcePrefix: ResourcePrefix.LINKS, + resourceKindProperty: LinkData.PROPERTIES, + property: dataLink!.properties + }); + dispatch(initAdvancedTabDialog(advanceDataLink)); + break; default: dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Could not open advanced tab for this resource.", hideDuration: 2000, kind: SnackbarKind.ERROR })); } @@ -582,5 +604,27 @@ const apiClientAuthorizationApiResponse = (apiResponse: ApiClientAuthorization) "default_owner_uuid": "${stringify(defaultOwnerUuid)}", "scopes": "${JSON.stringify(scopes, null, 4)}"`; + return response; +}; + +const linkApiResponse = (apiResponse: LinkResource) => { + const { + uuid, name, headUuid, properties, headKind, tailUuid, tailKind, linkClass, + ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid + } = apiResponse; + const response = `"uuid": "${uuid}", +"name": "${name}", +"head_uuid": "${headUuid}", +"head_kind": "${headKind}", +"tail_uuid": "${tailUuid}", +"tail_kind": "${tailKind}", +"link_class": "${linkClass}", +"owner_uuid": "${ownerUuid}", +"created_at": "${stringify(createdAt)}", +"modified_at": ${stringify(modifiedAt)}, +"modified_by_client_uuid": ${stringify(modifiedByClientUuid)}, +"modified_by_user_uuid": ${stringify(modifiedByUserUuid)}, +"properties": "${JSON.stringify(properties, null, 4)}"`; + return response; }; \ No newline at end of file diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts index b7d6cb26..e9b08a84 100644 --- a/src/store/context-menu/context-menu-actions.ts +++ b/src/store/context-menu/context-menu-actions.ts @@ -200,6 +200,8 @@ export const resourceKindToContextMenuKind = (uuid: string) => { return ContextMenuKind.PROCESS_RESOURCE; case ResourceKind.USER: return ContextMenuKind.ROOT_PROJECT; + case ResourceKind.LINK: + return ContextMenuKind.LINK; default: return; } diff --git a/src/store/link-panel/link-panel-actions.ts b/src/store/link-panel/link-panel-actions.ts new file mode 100644 index 00000000..944c1bd1 --- /dev/null +++ b/src/store/link-panel/link-panel-actions.ts @@ -0,0 +1,57 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { Dispatch } from 'redux'; +import { RootState } from '~/store/store'; +import { ServiceRepository } from '~/services/services'; +import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action'; +import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions'; +import { dialogActions } from '~/store/dialog/dialog-actions'; +import { LinkResource } from '~/models/link'; +import { getResource } from '~/store/resources/resources'; +import { snackbarActions } from '~/store/snackbar/snackbar-actions'; + +export const LINK_PANEL_ID = "linkPanelId"; +export const linkPanelActions = bindDataExplorerActions(LINK_PANEL_ID); + +export const LINK_REMOVE_DIALOG = 'linkRemoveDialog'; +export const LINK_ATTRIBUTES_DIALOG = 'linkAttributesDialog'; + +export const openLinkAttributesDialog = (uuid: string) => + (dispatch: Dispatch, getState: () => RootState) => { + const { resources } = getState(); + const link = getResource(uuid)(resources); + dispatch(dialogActions.OPEN_DIALOG({ id: LINK_ATTRIBUTES_DIALOG, data: { link } })); + }; + +export const openLinkRemoveDialog = (uuid: string) => + (dispatch: Dispatch, getState: () => RootState) => { + dispatch(dialogActions.OPEN_DIALOG({ + id: LINK_REMOVE_DIALOG, + data: { + title: 'Remove link', + text: 'Are you sure you want to remove this link?', + confirmButtonLabel: 'Remove', + uuid + } + })); + }; + +export const loadLinkPanel = () => + (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(setBreadcrumbs([{ label: 'Links' }])); + dispatch(linkPanelActions.REQUEST_ITEMS()); + }; + +export const removeLink = (uuid: string) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' })); + try { + await services.linkService.delete(uuid); + dispatch(linkPanelActions.REQUEST_ITEMS()); + dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Link has been successfully removed.', hideDuration: 2000 })); + } catch (e) { + return; + } + }; \ No newline at end of file diff --git a/src/store/link-panel/link-panel-middleware-service.ts b/src/store/link-panel/link-panel-middleware-service.ts new file mode 100644 index 00000000..b4d342c3 --- /dev/null +++ b/src/store/link-panel/link-panel-middleware-service.ts @@ -0,0 +1,70 @@ +// 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, dataExplorerToListParams, listResultsToDataExplorerItemsMeta } from '~/store/data-explorer/data-explorer-middleware-service'; +import { RootState } from '~/store/store'; +import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions'; +import { DataExplorer, getDataExplorer } from '~/store/data-explorer/data-explorer-reducer'; +import { updateResources } from '~/store/resources/resources-actions'; +import { SortDirection } from '~/components/data-table/data-column'; +import { OrderDirection, OrderBuilder } from '~/services/api/order-builder'; +import { ListResults } from '~/services/common-service/common-service'; +import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer"; +import { LinkResource } from '~/models/link'; +import { linkPanelActions } from '~/store/link-panel/link-panel-actions'; +import { LinkPanelColumnNames } from '~/views/link-panel/link-panel-root'; + +export class LinkMiddlewareService extends DataExplorerMiddlewareService { + constructor(private services: ServiceRepository, id: string) { + super(id); + } + + async requestItems(api: MiddlewareAPI) { + const state = api.getState(); + const dataExplorer = getDataExplorer(state.dataExplorer, this.getId()); + try { + const response = await this.services.linkService.list(getParams(dataExplorer)); + api.dispatch(updateResources(response.items)); + api.dispatch(setItems(response)); + } catch { + api.dispatch(couldNotFetchLinks()); + } + } +} + +export const getParams = (dataExplorer: DataExplorer) => ({ + ...dataExplorerToListParams(dataExplorer), + order: getOrder(dataExplorer) +}); + +const getOrder = (dataExplorer: DataExplorer) => { + const sortColumn = getSortColumn(dataExplorer); + const order = new OrderBuilder(); + if (sortColumn) { + const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC + ? OrderDirection.ASC + : OrderDirection.DESC; + + const columnName = sortColumn && sortColumn.name === LinkPanelColumnNames.NAME ? "name" : "modifiedAt"; + return order + .addOrder(sortDirection, columnName) + .getOrder(); + } else { + return order.getOrder(); + } +}; + +export const setItems = (listResults: ListResults) => + linkPanelActions.SET_ITEMS({ + ...listResultsToDataExplorerItemsMeta(listResults), + items: listResults.items.map(resource => resource.uuid), + }); + +const couldNotFetchLinks = () => + snackbarActions.OPEN_SNACKBAR({ + message: 'Could not fetch links.', + kind: SnackbarKind.ERROR + }); diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts index 8d68a4b6..0221fa9c 100644 --- a/src/store/navigation/navigation-action.ts +++ b/src/store/navigation/navigation-action.ts @@ -77,3 +77,5 @@ export const navigateToComputeNodes = push(Routes.COMPUTE_NODES); export const navigateToUsers = push(Routes.USERS); export const navigateToApiClientAuthorizations = push(Routes.API_CLIENT_AUTHORIZATIONS); + +export const navigateToLinks = push(Routes.LINKS); \ No newline at end of file diff --git a/src/store/store.ts b/src/store/store.ts index 2b0ada81..792224d2 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -50,6 +50,8 @@ import { UserMiddlewareService } from '~/store/users/user-panel-middleware-servi import { USERS_PANEL_ID } from '~/store/users/users-actions'; import { computeNodesReducer } from '~/store/compute-nodes/compute-nodes-reducer'; import { apiClientAuthorizationsReducer } from '~/store/api-client-authorizations/api-client-authorizations-reducer'; +import { LINK_PANEL_ID } from '~/store/link-panel/link-panel-actions'; +import { LinkMiddlewareService } from '~/store/link-panel/link-panel-middleware-service'; const composeEnhancers = (process.env.NODE_ENV === 'development' && @@ -84,7 +86,9 @@ export function configureStore(history: History, services: ServiceRepository): R const userPanelMiddleware = dataExplorerMiddleware( new UserMiddlewareService(services, USERS_PANEL_ID) ); - + const linkPanelMiddleware = dataExplorerMiddleware( + new LinkMiddlewareService(services, LINK_PANEL_ID) + ); const middlewares: Middleware[] = [ routerMiddleware(history), thunkMiddleware.withExtraArgument(services), @@ -94,7 +98,8 @@ export function configureStore(history: History, services: ServiceRepository): R searchResultsPanelMiddleware, sharedWithMePanelMiddleware, workflowPanelMiddleware, - userPanelMiddleware + userPanelMiddleware, + linkPanelMiddleware ]; const enhancer = composeEnhancers(applyMiddleware(...middlewares)); return createStore(rootReducer, enhancer); diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts index bc5eac6c..85540f0b 100644 --- a/src/store/workbench/workbench-actions.ts +++ b/src/store/workbench/workbench-actions.ts @@ -59,6 +59,8 @@ import { loadVirtualMachinesPanel } from '~/store/virtual-machines/virtual-machi import { loadRepositoriesPanel } from '~/store/repositories/repositories-actions'; import { loadKeepServicesPanel } from '~/store/keep-services/keep-services-actions'; import { loadUsersPanel, userBindedActions } from '~/store/users/users-actions'; +import { loadLinkPanel, linkPanelActions } from '~/store/link-panel/link-panel-actions'; +import { linkPanelColumns } from '~/views/link-panel/link-panel-root'; import { userPanelColumns } from '~/views/user-panel/user-panel'; import { loadComputeNodesPanel } from '~/store/compute-nodes/compute-nodes-actions'; import { loadApiClientAuthorizationsPanel } from '~/store/api-client-authorizations/api-client-authorizations-actions'; @@ -96,6 +98,7 @@ export const loadWorkbench = () => dispatch(workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns })); dispatch(searchResultsPanelActions.SET_COLUMNS({ columns: searchResultsPanelColumns })); dispatch(userBindedActions.SET_COLUMNS({ columns: userPanelColumns })); + dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns })); dispatch(initSidePanelTree()); if (router.location) { const match = matchRootRoute(router.location.pathname); @@ -402,6 +405,11 @@ export const loadSearchResults = handleFirstTimeLoad( await dispatch(loadSearchResultsPanel()); }); +export const loadLinks = handleFirstTimeLoad( + async (dispatch: Dispatch) => { + await dispatch(loadLinkPanel()); + }); + export const loadVirtualMachines = handleFirstTimeLoad( async (dispatch: Dispatch) => { await dispatch(loadVirtualMachinesPanel()); diff --git a/src/views-components/context-menu/action-sets/link-action-set.ts b/src/views-components/context-menu/action-sets/link-action-set.ts new file mode 100644 index 00000000..326741a2 --- /dev/null +++ b/src/views-components/context-menu/action-sets/link-action-set.ts @@ -0,0 +1,28 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { openLinkAttributesDialog, openLinkRemoveDialog } from '~/store/link-panel/link-panel-actions'; +import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab'; +import { ContextMenuActionSet } from "~/views-components/context-menu/context-menu-action-set"; +import { AdvancedIcon, RemoveIcon, AttributesIcon } from "~/components/icon/icon"; + +export const linkActionSet: ContextMenuActionSet = [[{ + name: "Attributes", + icon: AttributesIcon, + execute: (dispatch, { uuid }) => { + dispatch(openLinkAttributesDialog(uuid)); + } +}, { + name: "Advanced", + icon: AdvancedIcon, + execute: (dispatch, { uuid }) => { + dispatch(openAdvancedTabDialog(uuid)); + } +}, { + name: "Remove", + icon: RemoveIcon, + execute: (dispatch, { uuid }) => { + dispatch(openLinkRemoveDialog(uuid)); + } +}]]; diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx index 95a4a83f..a9200ebb 100644 --- a/src/views-components/context-menu/context-menu.tsx +++ b/src/views-components/context-menu/context-menu.tsx @@ -75,5 +75,6 @@ export enum ContextMenuKind { VIRTUAL_MACHINE = "VirtualMachine", KEEP_SERVICE = "KeepService", USER = "User", + LINK = "Link", NODE = "Node" } diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx index 16ea7a99..1be47be7 100644 --- a/src/views-components/data-explorer/renderers.tsx +++ b/src/views-components/data-explorer/renderers.tsx @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; -import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox } from '@material-ui/core'; +import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox, Button } from '@material-ui/core'; import { FavoriteStar } from '../favorite-star/favorite-star'; import { ResourceKind, TrashableResource } from '~/models/resource'; import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon, WorkflowIcon, ShareIcon } from '~/components/icon/icon'; @@ -23,6 +23,9 @@ import { getResourceData } from "~/store/resources-data/resources-data"; import { openSharingDialog } from '~/store/sharing-dialog/sharing-dialog-actions'; import { UserResource } from '~/models/user'; import { toggleIsActive, toggleIsAdmin } from '~/store/users/users-actions'; +import { LinkResource } from '~/models/link'; +import { navigateTo } from '~/store/navigation/navigation-action'; +import { Link } from 'react-router-dom'; const renderName = (item: { name: string; uuid: string, kind: string }) => @@ -119,6 +122,7 @@ const renderFirstName = (item: { firstName: string }) => { return {item.firstName}; }; +// User Resources export const ResourceFirstName = connect( (state: RootState, props: { uuid: string }) => { const resource = getResource(props.uuid)(state.resources); @@ -187,6 +191,60 @@ export const ResourceUsername = connect( return resource || { username: '' }; })(renderUsername); +// Links Resources +const renderLinkName = (item: { name: string }) => + {item.name || '(none)'}; + +export const ResourceLinkName = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources); + return resource || { name: '' }; + })(renderLinkName); + +const renderLinkClass = (item: { linkClass: string }) => + {item.linkClass}; + +export const ResourceLinkClass = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources); + return resource || { linkClass: '' }; + })(renderLinkClass); + +const renderLinkTail = (dispatch: Dispatch, item: { uuid: string, tailUuid: string, tailKind: string }) => + dispatch(navigateTo(item.uuid))}> + {resourceLabel(item.tailKind)}: {item.tailUuid} + ; + +export const ResourceLinkTail = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources); + return { + item: resource || { uuid: '', tailUuid: '', tailKind: ResourceKind.NONE } + }; + })((props: { item: any } & DispatchProp) => + renderLinkTail(props.dispatch, props.item)); + +const renderLinkHead = (dispatch: Dispatch, item: { uuid: string, headUuid: string, headKind: ResourceKind }) => + dispatch(navigateTo(item.uuid))}> + {resourceLabel(item.headKind)}: {item.headUuid} + ; + +export const ResourceLinkHead = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources); + return { + item: resource || { uuid: '', headUuid: '', headKind: ResourceKind.NONE } + }; + })((props: { item: any } & DispatchProp) => + renderLinkHead(props.dispatch, props.item)); + +export const ResourceLinkUuid = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources); + return resource || { uuid: '' }; + })(renderUuid); + +// Process Resources const resourceRunProcess = (dispatch: Dispatch, uuid: string) => { return (
diff --git a/src/views-components/links-dialog/attributes-dialog.tsx b/src/views-components/links-dialog/attributes-dialog.tsx new file mode 100644 index 00000000..8226c621 --- /dev/null +++ b/src/views-components/links-dialog/attributes-dialog.tsx @@ -0,0 +1,73 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from "react"; +import { compose } from 'redux'; +import { withStyles, Dialog, DialogTitle, DialogContent, DialogActions, Button, StyleRulesCallback, WithStyles, Grid } from '@material-ui/core'; +import { WithDialogProps, withDialog } from "~/store/dialog/with-dialog"; +import { LINK_ATTRIBUTES_DIALOG } from '~/store/link-panel/link-panel-actions'; +import { ArvadosTheme } from '~/common/custom-theme'; +import { LinkResource } from '~/models/link'; + +type CssRules = 'root'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + root: { + fontSize: '0.875rem', + '& div:nth-child(odd)': { + textAlign: 'right', + color: theme.palette.grey["500"] + } + } +}); + +interface AttributesLinkDialogDataProps { + link: LinkResource; +} + +export const AttributesLinkDialog = compose( + withDialog(LINK_ATTRIBUTES_DIALOG), + withStyles(styles))( + ({ open, closeDialog, data, classes }: WithDialogProps & WithStyles) => + + Attributes + + {data.link && + Uuid + {data.link.uuid} + Name + {data.link.name} + Head uuid + {data.link.headUuid} + Head kind + {data.link.headKind} + Tail uuid + {data.link.tailUuid} + Link class + {data.link.linkClass} + Owner uuid + {data.link.ownerUuid} + Created at + {data.link.createdAt} + Modified at + {data.link.modifiedAt} + Modified by user uuid + {data.link.modifiedByUserUuid} + Modified by client uuid + {data.link.modifiedByClientUuid} + } + + + + + + ); \ No newline at end of file diff --git a/src/views-components/links-dialog/remove-dialog.tsx b/src/views-components/links-dialog/remove-dialog.tsx new file mode 100644 index 00000000..22660c6b --- /dev/null +++ b/src/views-components/links-dialog/remove-dialog.tsx @@ -0,0 +1,20 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 +import { Dispatch, compose } from 'redux'; +import { connect } from "react-redux"; +import { ConfirmationDialog } from "~/components/confirmation-dialog/confirmation-dialog"; +import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog"; +import { LINK_REMOVE_DIALOG, removeLink } from '~/store/link-panel/link-panel-actions'; + +const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps) => ({ + onConfirm: () => { + props.closeDialog(); + dispatch(removeLink(props.data.uuid)); + } +}); + +export const RemoveLinkDialog = compose( + withDialog(LINK_REMOVE_DIALOG), + connect(null, mapDispatchToProps) +)(ConfirmationDialog); \ No newline at end of file diff --git a/src/views-components/main-app-bar/account-menu.tsx b/src/views-components/main-app-bar/account-menu.tsx index 44b113df..f765a608 100644 --- a/src/views-components/main-app-bar/account-menu.tsx +++ b/src/views-components/main-app-bar/account-menu.tsx @@ -12,12 +12,8 @@ import { logout } from '~/store/auth/auth-action'; import { RootState } from "~/store/store"; import { openCurrentTokenDialog } from '~/store/current-token-dialog/current-token-dialog-actions'; import { openRepositoriesPanel } from "~/store/repositories/repositories-actions"; -import { - navigateToSshKeys, navigateToKeepServices, navigateToComputeNodes, - navigateToApiClientAuthorizations, navigateToMyAccount -} from '~/store/navigation/navigation-action'; +import * as NavigationAction from '~/store/navigation/navigation-action'; import { openVirtualMachines } from "~/store/virtual-machines/virtual-machines-actions"; -import { navigateToUsers } from '~/store/navigation/navigation-action'; interface AccountMenuProps { user?: User; @@ -40,12 +36,13 @@ export const AccountMenu = connect(mapStateToProps)( dispatch(openVirtualMachines())}>Virtual Machines dispatch(openRepositoriesPanel())}>Repositories dispatch(openCurrentTokenDialog)}>Current token - dispatch(navigateToSshKeys)}>Ssh Keys - dispatch(navigateToUsers)}>Users - { user.isAdmin && dispatch(navigateToApiClientAuthorizations)}>Api Tokens } - { user.isAdmin && dispatch(navigateToKeepServices)}>Keep Services } - { user.isAdmin && dispatch(navigateToComputeNodes)}>Compute Nodes } - dispatch(navigateToMyAccount)}>My account + dispatch(NavigationAction.navigateToSshKeys)}>Ssh Keys + dispatch(NavigationAction.navigateToUsers)}>Users + { user.isAdmin && dispatch(NavigationAction.navigateToApiClientAuthorizations)}>Api Tokens } + { user.isAdmin && dispatch(NavigationAction.navigateToKeepServices)}>Keep Services } + { user.isAdmin && dispatch(NavigationAction.navigateToComputeNodes)}>Compute Nodes } + { user.isAdmin && dispatch(NavigationAction.navigateToLinks)}>Links } + dispatch(NavigationAction.navigateToMyAccount)}>My account dispatch(logout())}>Logout : null); diff --git a/src/views-components/main-content-bar/main-content-bar.tsx b/src/views-components/main-content-bar/main-content-bar.tsx index 8c2e4ced..03362178 100644 --- a/src/views-components/main-content-bar/main-content-bar.tsx +++ b/src/views-components/main-content-bar/main-content-bar.tsx @@ -21,7 +21,8 @@ const isButtonVisible = ({ router }: RootState) => { return !Routes.matchWorkflowRoute(pathname) && !Routes.matchVirtualMachineRoute(pathname) && !Routes.matchRepositoriesRoute(pathname) && !Routes.matchSshKeysRoute(pathname) && !Routes.matchKeepServicesRoute(pathname) && !Routes.matchComputeNodesRoute(pathname) && - !Routes.matchApiClientAuthorizationsRoute(pathname) && !Routes.matchUsersRoute(pathname); + !Routes.matchApiClientAuthorizationsRoute(pathname) && !Routes.matchUsersRoute(pathname) && + !Routes.matchLinksRoute(pathname); }; export const MainContentBar = connect((state: RootState) => ({ diff --git a/src/views/link-panel/link-panel-root.tsx b/src/views/link-panel/link-panel-root.tsx new file mode 100644 index 00000000..73b53fc1 --- /dev/null +++ b/src/views/link-panel/link-panel-root.tsx @@ -0,0 +1,96 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from 'react'; +import { LINK_PANEL_ID } from '~/store/link-panel/link-panel-actions'; +import { DataExplorer } from '~/views-components/data-explorer/data-explorer'; +import { SortDirection } from '~/components/data-table/data-column'; +import { DataColumns } from '~/components/data-table/data-table'; +import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view'; +import { ResourcesState } from '~/store/resources/resources'; +import { ShareMeIcon } from '~/components/icon/icon'; +import { createTree } from '~/models/tree'; +import { + ResourceLinkUuid, ResourceLinkHead, ResourceLinkTail, + ResourceLinkClass, ResourceLinkName } +from '~/views-components/data-explorer/renderers'; + +export enum LinkPanelColumnNames { + NAME = "Name", + LINK_CLASS = "Link Class", + TAIL = "Tail", + HEAD = 'Head', + UUID = "UUID" +} + +export const linkPanelColumns: DataColumns = [ + { + name: LinkPanelColumnNames.NAME, + selected: true, + configurable: true, + sortDirection: SortDirection.NONE, + filters: createTree(), + render: uuid => + }, + { + name: LinkPanelColumnNames.LINK_CLASS, + selected: true, + configurable: true, + // sortDirection: SortDirection.NONE, + filters: createTree(), + render: uuid => + }, + { + name: LinkPanelColumnNames.TAIL, + selected: true, + configurable: true, + // sortDirection: SortDirection.NONE, + filters: createTree(), + render: uuid => + }, + { + name: LinkPanelColumnNames.HEAD, + selected: true, + configurable: true, + // sortDirection: SortDirection.NONE, + filters: createTree(), + render: uuid => + }, + { + name: LinkPanelColumnNames.UUID, + selected: true, + configurable: true, + // sortDirection: SortDirection.NONE, + filters: createTree(), + render: uuid => + } +]; + +export interface LinkPanelDataProps { + resources: ResourcesState; +} + +export interface LinkPanelActionProps { + onItemClick: (item: string) => void; + onContextMenu: (event: React.MouseEvent, item: string) => void; + onItemDoubleClick: (item: string) => void; +} + +export type LinkPanelProps = LinkPanelDataProps & LinkPanelActionProps; + +export const LinkPanelRoot = (props: LinkPanelProps) => { + return + }/>; +}; \ No newline at end of file diff --git a/src/views/link-panel/link-panel.tsx b/src/views/link-panel/link-panel.tsx new file mode 100644 index 00000000..2c3bf758 --- /dev/null +++ b/src/views/link-panel/link-panel.tsx @@ -0,0 +1,35 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { Dispatch } from "redux"; +import { connect } from "react-redux"; +import { RootState } from '~/store/store'; +import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions'; +import { LinkPanelRoot, LinkPanelActionProps } from '~/views/link-panel/link-panel-root'; +import { ResourceKind } from '~/models/resource'; + +const mapStateToProps = (state: RootState) => { + return { + resources: state.resources + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch): LinkPanelActionProps => ({ + onContextMenu: (event, resourceUuid) => { + const kind = resourceKindToContextMenuKind(resourceUuid); + if (kind) { + dispatch(openContextMenu(event, { + name: '', + uuid: resourceUuid, + ownerUuid: '', + kind: ResourceKind.LINK, + menuKind: kind + })); + } + }, + onItemClick: (resourceUuid: string) => { return; }, + onItemDoubleClick: uuid => { return; } +}); + +export const LinkPanel = connect(mapStateToProps, mapDispatchToProps)(LinkPanelRoot); \ No newline at end of file diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index 70f2a2dd..8181cdf9 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -55,6 +55,7 @@ import { RepositoriesPanel } from '~/views/repositories-panel/repositories-panel import { KeepServicePanel } from '~/views/keep-service-panel/keep-service-panel'; import { ComputeNodePanel } from '~/views/compute-node-panel/compute-node-panel'; import { ApiClientAuthorizationPanel } from '~/views/api-client-authorization-panel/api-client-authorization-panel'; +import { LinkPanel } from '~/views/link-panel/link-panel'; import { RepositoriesSampleGitDialog } from '~/views-components/repositories-sample-git-dialog/repositories-sample-git-dialog'; import { RepositoryAttributesDialog } from '~/views-components/repository-attributes-dialog/repository-attributes-dialog'; import { CreateRepositoryDialog } from '~/views-components/dialog-forms/create-repository-dialog'; @@ -64,11 +65,13 @@ import { PublicKeyDialog } from '~/views-components/ssh-keys-dialog/public-key-d import { RemoveApiClientAuthorizationDialog } from '~/views-components/api-client-authorizations-dialog/remove-dialog'; import { RemoveComputeNodeDialog } from '~/views-components/compute-nodes-dialog/remove-dialog'; import { RemoveKeepServiceDialog } from '~/views-components/keep-services-dialog/remove-dialog'; +import { RemoveLinkDialog } from '~/views-components/links-dialog/remove-dialog'; import { RemoveSshKeyDialog } from '~/views-components/ssh-keys-dialog/remove-dialog'; import { RemoveVirtualMachineDialog } from '~/views-components/virtual-machines-dialog/remove-dialog'; import { AttributesApiClientAuthorizationDialog } from '~/views-components/api-client-authorizations-dialog/attributes-dialog'; import { AttributesComputeNodeDialog } from '~/views-components/compute-nodes-dialog/attributes-dialog'; import { AttributesKeepServiceDialog } from '~/views-components/keep-services-dialog/attributes-dialog'; +import { AttributesLinkDialog } from '~/views-components/links-dialog/attributes-dialog'; import { AttributesSshKeyDialog } from '~/views-components/ssh-keys-dialog/attributes-dialog'; import { VirtualMachineAttributesDialog } from '~/views-components/virtual-machines-dialog/attributes-dialog'; import { UserPanel } from '~/views/user-panel/user-panel'; @@ -152,6 +155,7 @@ export const WorkbenchPanel = + @@ -164,6 +168,7 @@ export const WorkbenchPanel = + @@ -190,6 +195,7 @@ export const WorkbenchPanel = + -- 2.30.2