refs #13828 Merge branch 'origin/13828-trash-view'
authorDaniel Kos <daniel.kos@contractors.roche.com>
Tue, 4 Sep 2018 07:41:44 +0000 (09:41 +0200)
committerDaniel Kos <daniel.kos@contractors.roche.com>
Tue, 4 Sep 2018 07:49:11 +0000 (09:49 +0200)
# 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 <daniel.kos@contractors.roche.com>

19 files changed:
1  2 
src/common/formatters.ts
src/components/data-explorer/data-explorer.test.tsx
src/components/icon/icon.tsx
src/index.tsx
src/models/resource.ts
src/routes/routes.ts
src/services/common-service/common-resource-service.ts
src/services/groups-service/groups-service.test.ts
src/services/log-service/log-service.ts
src/store/context-menu/context-menu-actions.ts
src/store/navigation/navigation-action.ts
src/store/process-logs-panel/process-logs-panel-actions.ts
src/store/processes/processes-actions.ts
src/store/store.ts
src/store/workbench/workbench-actions.ts
src/views-components/context-menu/context-menu.tsx
src/views/workbench/workbench.tsx
src/websocket/websocket-service.ts
src/websocket/websocket.ts

Simple merge
index 3e447b4015480091b66e6ee39f5b6d5560ea6cc6,3e447b4015480091b66e6ee39f5b6d5560ea6cc6..882c178be5ef5550629412f5116a7ab3f4e6d50f
@@@ -124,4 -124,4 +124,5 @@@ const mockDataExplorerProps = () => (
      defaultIcon: ProjectIcon,
      onSetColumns: jest.fn(),
      defaultMessages: ['testing'],
++    contextMenuColumn: true
  });
Simple merge
diff --cc src/index.tsx
index 20d2c1e9e1f1fc2bf0ee31023a58583e92dc5988,6137e26dd9319e83d4973280c8dab04b9e209e1d..4ce80d31e9d0f0360e6dd7051b113f9dbfe4d40c
@@@ -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);
Simple merge
index 6901d8755588acb9d5f1600f30cca934ca05566c,d1193218bd24ae4d0491b92bc6efc651954f26ec..05a8ab099ce1db395fb42f67561f330f3adae289
@@@ -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));
      }
  };
index c3be8bdaa8a6ecfa58a1553b7de57483481ff86b,c3be8bdaa8a6ecfa58a1553b7de57483481ff86b..e1157f4b177e5ca18c9764c9bb249cf1467d7074
@@@ -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,
index c92475d55db24f77e81c8be0dd221aa6f409b663,0000000000000000000000000000000000000000..8f6c66c8a2ddbf24f9e02fb0fe84874036b23f6c
mode 100644,000000..100644
--- /dev/null
@@@ -1,13 -1,0 +1,13 @@@
- import { CommonResourceService } from "~/common/api/common-resource-service";
 +// Copyright (C) The Arvados Authors. All rights reserved.
 +//
 +// SPDX-License-Identifier: AGPL-3.0
 +
 +import { AxiosInstance } from "axios";
 +import { LogResource } from '~/models/log';
++import { CommonResourceService } from "~/services/common-service/common-resource-service";
 +
 +export class LogService extends CommonResourceService<LogResource> {
 +    constructor(serverApi: AxiosInstance) {
 +        super(serverApi, "logs");
 +    }
 +}
index 3440a3053dad930dd1db230e0b52e9100e690935,85e576112f60cc773aa471e248f30c79acb526c7..2b0e6f8f8bd2aad4192397459c79927862264166
@@@ -70,18 -85,13 +85,20 @@@ export const openSidePanelContextMenu 
  
  export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>) =>
      (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<any>(openContextMenu(event, resource));
      };
index 62c9a25dd34a306b6d2b8395b054cc6a837309c6,0000000000000000000000000000000000000000..79c6434c5bdf23d63a6efa6bfc4c775a4690e950
mode 100644,000000..100644
--- /dev/null
@@@ -1,114 -1,0 +1,114 @@@
- import { FilterBuilder } from '~/common/api/filter-builder';
 +// 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 { OrderBuilder } from '~/common/api/order-builder';
 +import { groupBy } from 'lodash';
 +import { loadProcess } from '~/store/processes/processes-actions';
- import { ResourceEventMessage } from '../../websocket/resource-event-message';
 +import { LogResource } from '~/models/log';
 +import { LogService } from '~/services/log-service/log-service';
++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<string>(),
 +    ADD_PROCESS_LOGS_PANEL_ITEM: ofType<{ logType: string, log: string }>(),
 +});
 +
 +export type ProcessLogsPanelAction = UnionOf<typeof processLogsPanelActions>;
 +
 +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<any>(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<LogResource>()
 +        .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,
 +];
index d94cc01e4cad412fab9056885eba44efaf2ab018,f026d37ed3b06bae0aed2ebd8bfe24f2883ab2a9..031683a7e8af5a48fbf6067de0455c9ee31f2dd3
@@@ -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<Process> => {
 +        const containerRequest = await services.containerRequestService.get(containerRequestUuid);
          dispatch<any>(updateResources([containerRequest]));
          if (containerRequest.containerUuid) {
              const container = await services.containerService.get(containerRequest.containerUuid);
index d0c0dd67b10453d3a11191ecd7418e10dc7b00bc,d2371e831768cb3b41811725709174a7416a9861..01aca598be44d384a052e4d4d5f85c6955e2d9b8
@@@ -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' &&
index 8c7ec9a30f96a25ed699a38d78a0a6c67eb32760,1ff3a5b56b1297d8e9fa66ba2003ade21656b723..80f50fe153744382832c81aa34d63f8976ea2869
@@@ -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 = () =>
index 21396d1d491306106bbe7210709ccd43ab69f2ef,68bb970073317f00844fdf04e74a388920d527f2..3c281087c2addf20ad8f3f6e7e31500673a28782
@@@ -167,7 -169,7 +169,8 @@@ export const Workbench = withStyles(sty
                                      <Route path={Routes.COLLECTIONS} component={CollectionPanel} />
                                      <Route path={Routes.FAVORITES} component={FavoritePanel} />
                                      <Route path={Routes.PROCESSES} component={ProcessPanel} />
+                                     <Route path={Routes.TRASH} component={TrashPanel} />
 +                                    <Route path={Routes.PROCESS_LOGS} component={ProcessLogPanel} />
                                  </Switch>
                              </div>
                              {user && <DetailsPanel />}
index 77c1fd323f9c24f2ff88514712cadaaaef3a4a1d,0000000000000000000000000000000000000000..a5ce13d7982aec92130cae83110fda3e0f5134b9
mode 100644,000000..100644
--- /dev/null
@@@ -1,47 -1,0 +1,47 @@@
- import { CommonResourceService } from '~/common/api/common-resource-service';
 +// 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 { 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"}');
 +    }
 +
 +}
index 634fa8f979b4387b0acdc8c23740ce33459e4ddc,0000000000000000000000000000000000000000..e3f1e192023e8acda5aa84db991010b48a83d3e4
mode 100644,000000..100644
--- /dev/null
@@@ -1,38 -1,0 +1,38 @@@
- import { loadContainers } from '../store/processes/processes-actions';
- import { FilterBuilder } from '~/common/api/filter-builder';
- import { LogEventType } from '../models/log';
 +// 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 { 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}>));
 +    }
 +};