From: Daniel Kos Date: Tue, 4 Sep 2018 07:41:44 +0000 (+0200) Subject: refs #13828 Merge branch 'origin/13828-trash-view' X-Git-Tag: 1.3.0~121 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/c276ce4c8dd3cf27ad6d17eca9af473f353fda55 refs #13828 Merge branch 'origin/13828-trash-view' # Conflicts: # src/index.tsx # src/routes/routes.ts # src/store/context-menu/context-menu-actions.ts # src/store/store.ts # src/store/workbench/workbench-actions.ts # src/views/workbench/workbench.tsx Arvados-DCO-1.1-Signed-off-by: Daniel Kos --- c276ce4c8dd3cf27ad6d17eca9af473f353fda55 diff --cc src/components/data-explorer/data-explorer.test.tsx index 3e447b40,3e447b40..882c178b --- a/src/components/data-explorer/data-explorer.test.tsx +++ b/src/components/data-explorer/data-explorer.test.tsx @@@ -124,4 -124,4 +124,5 @@@ const mockDataExplorerProps = () => ( defaultIcon: ProjectIcon, onSetColumns: jest.fn(), defaultMessages: ['testing'], ++ contextMenuColumn: true }); diff --cc src/index.tsx index 20d2c1e9,6137e26d..4ce80d31 --- a/src/index.tsx +++ b/src/index.tsx @@@ -31,9 -31,7 +31,10 @@@ import { processActionSet } from './vie import { addRouteChangeHandlers } from './routes/routes'; import { loadWorkbench } from './store/workbench/workbench-actions'; import { Routes } from '~/routes/routes'; + import { trashActionSet } from "~/views-components/context-menu/action-sets/trash-action-set"; +import { ServiceRepository } from '~/services/services'; +import { initWebSocket } from '~/websocket/websocket'; +import { Config } from '~/common/config'; const getBuildNumber = () => "BN-" + (process.env.REACT_APP_BUILD_NUMBER || "dev"); const getGitCommit = () => "GIT-" + (process.env.REACT_APP_GIT_COMMIT || "latest").substr(0, 7); diff --cc src/routes/routes.ts index 6901d875,d1193218..05a8ab09 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@@ -6,9 -6,9 +6,9 @@@ import { History, Location } from 'hist import { RootStore } from '~/store/store'; import { matchPath } from 'react-router'; import { ResourceKind, RESOURCE_UUID_PATTERN, extractUuidKind } from '~/models/resource'; - import { getProjectUrl } from '../models/project'; + import { getProjectUrl } from '~/models/project'; import { getCollectionUrl } from '~/models/collection'; - import { loadProject, loadFavorites, loadCollection, loadProcessLog } from '~/store/workbench/workbench-actions'; -import { loadProject, loadFavorites, loadCollection, loadTrash } from '~/store/workbench/workbench-actions'; ++import { loadProject, loadFavorites, loadCollection, loadTrash, loadProcessLog } from '~/store/workbench/workbench-actions'; import { loadProcess } from '~/store/processes/processes-actions'; export const Routes = { @@@ -18,7 -18,7 +18,8 @@@ COLLECTIONS: `/collections/:id(${RESOURCE_UUID_PATTERN})`, PROCESSES: `/processes/:id(${RESOURCE_UUID_PATTERN})`, FAVORITES: '/favorites', - TRASH: '/trash' ++ TRASH: '/trash', + PROCESS_LOGS: `/process-logs/:id(${RESOURCE_UUID_PATTERN})` }; export const getResourceUrl = (uuid: string) => { @@@ -69,18 -68,17 +73,21 @@@ const handleLocationChange = (store: Ro const projectMatch = matchProjectRoute(pathname); const collectionMatch = matchCollectionRoute(pathname); const favoriteMatch = matchFavoritesRoute(pathname); + const trashMatch = matchTrashRoute(pathname); const processMatch = matchProcessRoute(pathname); + const processLogMatch = matchProcessLogRoute(pathname); + if (projectMatch) { store.dispatch(loadProject(projectMatch.params.id)); } else if (collectionMatch) { store.dispatch(loadCollection(collectionMatch.params.id)); } else if (favoriteMatch) { store.dispatch(loadFavorites()); + } else if (trashMatch) { + store.dispatch(loadTrash()); } else if (processMatch) { store.dispatch(loadProcess(processMatch.params.id)); + } else if (processLogMatch) { + store.dispatch(loadProcessLog(processLogMatch.params.id)); } }; diff --cc src/services/groups-service/groups-service.test.ts index c3be8bda,c3be8bda..e1157f4b --- a/src/services/groups-service/groups-service.test.ts +++ b/src/services/groups-service/groups-service.test.ts @@@ -16,7 -16,7 +16,7 @@@ describe("GroupsService", () => it("#contents", async () => { axiosMock -- .onGet("/groups/1/contents/") ++ .onGet("/groups/1/contents") .reply(200, { kind: "kind", offset: 2, diff --cc src/services/log-service/log-service.ts index c92475d5,00000000..8f6c66c8 mode 100644,000000..100644 --- a/src/services/log-service/log-service.ts +++ b/src/services/log-service/log-service.ts @@@ -1,13 -1,0 +1,13 @@@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + - import { CommonResourceService } from "~/common/api/common-resource-service"; +import { AxiosInstance } from "axios"; +import { LogResource } from '~/models/log'; ++import { CommonResourceService } from "~/services/common-service/common-resource-service"; + +export class LogService extends CommonResourceService { + constructor(serverApi: AxiosInstance) { + super(serverApi, "logs"); + } +} diff --cc src/store/context-menu/context-menu-actions.ts index 3440a305,85e57611..2b0e6f8f --- a/src/store/context-menu/context-menu-actions.ts +++ b/src/store/context-menu/context-menu-actions.ts @@@ -70,18 -85,13 +85,20 @@@ export const openSidePanelContextMenu export const openProcessContextMenu = (event: React.MouseEvent) => (dispatch: Dispatch, getState: () => RootState) => { + const { location } = getState().router; + const pathname = location ? location.pathname : ''; + // ToDo: We get error from matchProcessRoute + // const match = matchProcessRoute(pathname); + // console.log('match: ', match); + // const uuid = match ? match.params.id : ''; + const uuid = pathname.split('/').slice(-1)[0]; const resource = { - uuid, + uuid: '', + ownerUuid: '', + kind: ResourceKind.PROCESS, name: '', description: '', - kind: ContextMenuKind.PROCESS + menuKind: ContextMenuKind.PROCESS }; dispatch(openContextMenu(event, resource)); }; diff --cc src/store/process-logs-panel/process-logs-panel-actions.ts index 62c9a25d,00000000..79c6434c mode 100644,000000..100644 --- a/src/store/process-logs-panel/process-logs-panel-actions.ts +++ b/src/store/process-logs-panel/process-logs-panel-actions.ts @@@ -1,114 -1,0 +1,114 @@@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { unionize, ofType, UnionOf } from "~/common/unionize"; +import { ProcessLogs, getProcessLogsPanelCurrentUuid } from './process-logs-panel'; +import { LogEventType } from '~/models/log'; +import { RootState } from '~/store/store'; +import { ServiceRepository } from '~/services/services'; +import { Dispatch } from 'redux'; - import { FilterBuilder } from '~/common/api/filter-builder'; +import { groupBy } from 'lodash'; +import { loadProcess } from '~/store/processes/processes-actions'; - import { OrderBuilder } from '~/common/api/order-builder'; +import { LogResource } from '~/models/log'; +import { LogService } from '~/services/log-service/log-service'; - import { ResourceEventMessage } from '../../websocket/resource-event-message'; ++import { ResourceEventMessage } from '~/websocket/resource-event-message'; +import { getProcess } from '~/store/processes/process'; ++import { FilterBuilder } from "~/services/api/filter-builder"; ++import { OrderBuilder } from "~/services/api/order-builder"; + +export const processLogsPanelActions = unionize({ + RESET_PROCESS_LOGS_PANEL: ofType<{}>(), + INIT_PROCESS_LOGS_PANEL: ofType<{ filters: string[], logs: ProcessLogs }>(), + SET_PROCESS_LOGS_PANEL_FILTER: ofType(), + ADD_PROCESS_LOGS_PANEL_ITEM: ofType<{ logType: string, log: string }>(), +}); + +export type ProcessLogsPanelAction = UnionOf; + +export const setProcessLogsPanelFilter = (filter: string) => + processLogsPanelActions.SET_PROCESS_LOGS_PANEL_FILTER(filter); + +export const initProcessLogsPanel = (processUuid: string) => + async (dispatch: Dispatch, getState: () => RootState, { logService }: ServiceRepository) => { + dispatch(processLogsPanelActions.RESET_PROCESS_LOGS_PANEL()); + const process = await dispatch(loadProcess(processUuid)); + if (process.container) { + const logResources = await loadContainerLogs(process.container.uuid, logService); + const initialState = createInitialLogPanelState(logResources); + dispatch(processLogsPanelActions.INIT_PROCESS_LOGS_PANEL(initialState)); + } + }; + +export const addProcessLogsPanelItem = (message: ResourceEventMessage<{ text: string }>) => + async (dispatch: Dispatch, getState: () => RootState, { logService }: ServiceRepository) => { + if (PROCESS_PANEL_LOG_EVENT_TYPES.indexOf(message.eventType) > -1) { + const uuid = getProcessLogsPanelCurrentUuid(getState()); + if (uuid) { + const process = getProcess(uuid)(getState().resources); + if (process) { + const { containerRequest, container } = process; + if (message.objectUuid === containerRequest.uuid + || container && message.objectUuid === container.uuid) { + dispatch(processLogsPanelActions.ADD_PROCESS_LOGS_PANEL_ITEM({ + logType: SUMMARIZED_FILTER_TYPE, + log: message.properties.text + })); + dispatch(processLogsPanelActions.ADD_PROCESS_LOGS_PANEL_ITEM({ + logType: message.eventType, + log: message.properties.text + })); + } + } + } + } + }; + +const loadContainerLogs = async (containerUuid: string, logService: LogService) => { + const requestFilters = new FilterBuilder() + .addEqual('objectUuid', containerUuid) + .addIn('eventType', PROCESS_PANEL_LOG_EVENT_TYPES) + .getFilters(); + const requestOrder = new OrderBuilder() + .addAsc('eventAt') + .getOrder(); + const requestParams = { + limit: MAX_AMOUNT_OF_LOGS, + filters: requestFilters, + order: requestOrder, + }; + const { items } = await logService.list(requestParams); + return items; +}; + +const createInitialLogPanelState = (logResources: LogResource[]) => { + const allLogs = logsToLines(logResources); + const groupedLogResources = groupBy(logResources, log => log.eventType); + const groupedLogs = Object + .keys(groupedLogResources) + .reduce((grouped, key) => ({ + ...grouped, + [key]: logsToLines(groupedLogResources[key]) + }), {}); + const filters = [SUMMARIZED_FILTER_TYPE, ...Object.keys(groupedLogs)]; + const logs = { [SUMMARIZED_FILTER_TYPE]: allLogs, ...groupedLogs }; + return { filters, logs }; +}; + +const logsToLines = (logs: LogResource[]) => + logs.map(({ properties }) => properties.text); + +const MAX_AMOUNT_OF_LOGS = 10000; + +const SUMMARIZED_FILTER_TYPE = 'Summarized'; + +const PROCESS_PANEL_LOG_EVENT_TYPES = [ + LogEventType.ARV_MOUNT, + LogEventType.CRUNCH_RUN, + LogEventType.CRUNCHSTAT, + LogEventType.DISPATCH, + LogEventType.HOSTSTAT, + LogEventType.NODE_INFO, + LogEventType.STDERR, + LogEventType.STDOUT, +]; diff --cc src/store/processes/processes-actions.ts index d94cc01e,f026d37e..031683a7 --- a/src/store/processes/processes-actions.ts +++ b/src/store/processes/processes-actions.ts @@@ -6,13 -6,12 +6,13 @@@ import { Dispatch } from "redux" import { RootState } from '~/store/store'; import { ServiceRepository } from '~/services/services'; import { updateResources } from '~/store/resources/resources-actions'; - import { FilterBuilder } from '~/common/api/filter-builder'; + import { FilterBuilder } from '~/services/api/filter-builder'; import { ContainerRequestResource } from '../../models/container-request'; +import { Process } from './process'; -export const loadProcess = (uuid: string) => - async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - const containerRequest = await services.containerRequestService.get(uuid); +export const loadProcess = (containerRequestUuid: string) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise => { + const containerRequest = await services.containerRequestService.get(containerRequestUuid); dispatch(updateResources([containerRequest])); if (containerRequest.containerUuid) { const container = await services.containerService.get(containerRequest.containerUuid); diff --cc src/store/store.ts index d0c0dd67,d2371e83..01aca598 --- a/src/store/store.ts +++ b/src/store/store.ts @@@ -28,7 -28,8 +28,9 @@@ import { resourcesReducer } from '~/sto import { propertiesReducer } from './properties/properties-reducer'; import { RootState } from './store'; import { fileUploaderReducer } from './file-uploader/file-uploader-reducer'; + import { TrashPanelMiddlewareService } from "~/store/trash-panel/trash-panel-middleware-service"; + import { TRASH_PANEL_ID } from "~/store/trash-panel/trash-panel-action"; +import { processLogsPanelReducer } from './process-logs-panel/process-logs-panel-reducer'; const composeEnhancers = (process.env.NODE_ENV === 'development' && diff --cc src/store/workbench/workbench-actions.ts index 8c7ec9a3,1ff3a5b5..80f50fe1 --- a/src/store/workbench/workbench-actions.ts +++ b/src/store/workbench/workbench-actions.ts @@@ -30,7 -30,8 +30,9 @@@ import * as collectionUpdateActions fro import * as collectionMoveActions from '~/store/collections/collection-move-actions'; import * as processesActions from '../processes/processes-actions'; import { getProcess } from '../processes/process'; + import { trashPanelColumns } from "~/views/trash-panel/trash-panel"; + import { loadTrashPanel, trashPanelActions } from "~/store/trash-panel/trash-panel-action"; +import { initProcessLogsPanel } from '../process-logs-panel/process-logs-panel-actions'; export const loadWorkbench = () => diff --cc src/views/workbench/workbench.tsx index 21396d1d,68bb9700..3c281087 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@@ -167,7 -169,7 +169,8 @@@ export const Workbench = withStyles(sty + + {user && } diff --cc src/websocket/websocket-service.ts index 77c1fd32,00000000..a5ce13d7 mode 100644,000000..100644 --- a/src/websocket/websocket-service.ts +++ b/src/websocket/websocket-service.ts @@@ -1,47 -1,0 +1,47 @@@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { AuthService } from '~/services/auth-service/auth-service'; +import { ResourceEventMessage } from './resource-event-message'; - import { CommonResourceService } from '~/common/api/common-resource-service'; +import { camelCase } from 'lodash'; ++import { CommonResourceService } from "~/services/common-service/common-resource-service"; + +type MessageListener = (message: ResourceEventMessage) => void; + +export class WebSocketService { + private ws: WebSocket; + private messageListener: MessageListener; + + constructor(private url: string, private authService: AuthService) { } + + connect() { + if (this.ws) { + this.ws.close(); + } + this.ws = new WebSocket(this.getUrl()); + this.ws.addEventListener('message', this.handleMessage); + this.ws.addEventListener('open', this.handleOpen); + } + + setMessageListener = (listener: MessageListener) => { + this.messageListener = listener; + } + + private getUrl() { + return `${this.url}?api_token=${this.authService.getApiToken()}`; + } + + private handleMessage = (event: MessageEvent) => { + if (this.messageListener) { + const data = JSON.parse(event.data); + const message = CommonResourceService.mapKeys(camelCase)(data); + this.messageListener(message); + } + } + + private handleOpen = () => { + this.ws.send('{"method":"subscribe"}'); + } + +} diff --cc src/websocket/websocket.ts index 634fa8f9,00000000..e3f1e192 mode 100644,000000..100644 --- a/src/websocket/websocket.ts +++ b/src/websocket/websocket.ts @@@ -1,38 -1,0 +1,38 @@@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { RootStore } from '~/store/store'; +import { AuthService } from '~/services/auth-service/auth-service'; +import { Config } from '~/common/config'; +import { WebSocketService } from './websocket-service'; +import { ResourceEventMessage } from './resource-event-message'; +import { ResourceKind } from '~/models/resource'; +import { loadProcess } from '~/store/processes/processes-actions'; - import { loadContainers } from '../store/processes/processes-actions'; - import { FilterBuilder } from '~/common/api/filter-builder'; - import { LogEventType } from '../models/log'; ++import { loadContainers } from '~/store/processes/processes-actions'; ++import { LogEventType } from '~/models/log'; +import { addProcessLogsPanelItem } from '../store/process-logs-panel/process-logs-panel-actions'; ++import { FilterBuilder } from "~/services/api/filter-builder"; + +export const initWebSocket = (config: Config, authService: AuthService, store: RootStore) => { + const webSocketService = new WebSocketService(config.websocketUrl, authService); + webSocketService.setMessageListener(messageListener(store)); + webSocketService.connect(); +}; + +const messageListener = (store: RootStore) => (message: ResourceEventMessage) => { + if (message.eventType === LogEventType.CREATE || message.eventType === LogEventType.UPDATE) { + switch (message.objectKind) { + case ResourceKind.CONTAINER_REQUEST: + return store.dispatch(loadProcess(message.objectUuid)); + case ResourceKind.CONTAINER: + return store.dispatch(loadContainers( + new FilterBuilder().addIn('uuid', [message.objectUuid]).getFilters() + )); + default: + return; + } + } else { + return store.dispatch(addProcessLogsPanelItem(message as ResourceEventMessage<{text: string}>)); + } +};