//
// SPDX-License-Identifier: AGPL-3.0
- export const formatDate = (isoDate: string) => {
- const date = new Date(isoDate);
- const text = date.toLocaleString();
- return text === 'Invalid Date' ? "" : text;
+ export const formatDate = (isoDate?: string) => {
+ if (isoDate) {
+ const date = new Date(isoDate);
+ const text = date.toLocaleString();
+ return text === 'Invalid Date' ? "" : text;
+ }
+ return "";
};
export const formatFileSize = (size?: number) => {
return "";
};
+export const formatTime = (time: number) => {
+ const minutes = Math.floor(time / (1000 * 60) % 60).toFixed(0);
+ const hours = Math.floor(time / (1000 * 60 * 60)).toFixed(0);
+
+ return hours + "h " + minutes + "m";
+};
+
+export const getTimeDiff = (endTime: string, startTime: string) => {
+ return new Date(endTime).getTime() - new Date(startTime).getTime();
+};
+
export const formatProgress = (loaded: number, total: number) => {
const progress = loaded >= 0 && total > 0 ? loaded * 100 / total : 0;
return `${progress.toFixed(2)}%`;
defaultIcon: ProjectIcon,
onSetColumns: jest.fn(),
defaultMessages: ['testing'],
++ contextMenuColumn: true
});
import * as React from 'react';
import AccessTime from '@material-ui/icons/AccessTime';
+import ArrowBack from '@material-ui/icons/ArrowBack';
import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
import BubbleChart from '@material-ui/icons/BubbleChart';
import Cached from '@material-ui/icons/Cached';
import PersonAdd from '@material-ui/icons/PersonAdd';
import PlayArrow from '@material-ui/icons/PlayArrow';
import RateReview from '@material-ui/icons/RateReview';
+ import RestoreFromTrash from '@material-ui/icons/History';
import Search from '@material-ui/icons/Search';
import SettingsApplications from '@material-ui/icons/SettingsApplications';
import Star from '@material-ui/icons/Star';
export const AddFavoriteIcon: IconType = (props) => <StarBorder {...props} />;
export const AdvancedIcon: IconType = (props) => <SettingsApplications {...props} />;
+export const BackIcon: IconType = (props) => <ArrowBack {...props} />;
export const CustomizeTableIcon: IconType = (props) => <Menu {...props} />;
export const CopyIcon: IconType = (props) => <ContentCopy {...props} />;
export const CollectionIcon: IconType = (props) => <LibraryBooks {...props} />;
export const RemoveIcon: IconType = (props) => <Delete {...props} />;
export const RemoveFavoriteIcon: IconType = (props) => <Star {...props} />;
export const RenameIcon: IconType = (props) => <Edit {...props} />;
+ export const RestoreFromTrashIcon: IconType = (props) => <RestoreFromTrash {...props} />;
export const ReRunProcessIcon: IconType = (props) => <Cached {...props} />;
export const SearchIcon: IconType = (props) => <Search {...props} />;
export const ShareIcon: IconType = (props) => <PersonAdd {...props} />;
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);
addMenuActionSet(ContextMenuKind.COLLECTION, collectionActionSet);
addMenuActionSet(ContextMenuKind.COLLECTION_RESOURCE, collectionResourceActionSet);
addMenuActionSet(ContextMenuKind.PROCESS, processActionSet);
+ addMenuActionSet(ContextMenuKind.TRASH, trashActionSet);
fetchConfig()
.then((config) => {
const services = createServices(config);
const store = configureStore(history, services);
- store.subscribe(initListener(history, store));
-
+ store.subscribe(initListener(history, store, services, config));
store.dispatch(initAuth());
const TokenComponent = (props: any) => <ApiToken authService={services.authService} {...props} />;
});
-const initListener = (history: History, store: RootStore) => {
+const initListener = (history: History, store: RootStore, services: ServiceRepository, config: Config) => {
let initialized = false;
return async () => {
const { router, auth } = store.getState();
if (router.location && auth.user && !initialized) {
initialized = true;
+ initWebSocket(config, services.authService, store);
await store.dispatch(loadWorkbench());
addRouteChangeHandlers(history, store);
}
modifiedByUserUuid: string;
modifiedAt: string;
href: string;
- kind: string;
+ kind: ResourceKind;
etag: string;
}
+ export interface TrashableResource extends Resource {
+ trashAt: string;
+ deleteAt: string;
+ isTrashed: boolean;
+ }
+
export enum ResourceKind {
COLLECTION = "arvados#collection",
CONTAINER = "arvados#container",
CONTAINER_REQUEST = "arvados#containerRequest",
GROUP = "arvados#group",
+ LOG = "arvados#log",
PROCESS = "arvados#containerRequest",
PROJECT = "arvados#group",
USER = "arvados#user",
WORKFLOW = "arvados#workflow",
+ NONE = "arvados#none"
}
export enum ResourceObjectType {
CONTAINER = 'dz642',
CONTAINER_REQUEST = 'xvhdp',
GROUP = 'j7d0g',
+ LOG = '57u5n',
USER = 'tpzed',
}
return ResourceKind.CONTAINER_REQUEST;
case ResourceObjectType.CONTAINER:
return ResourceKind.CONTAINER;
+ case ResourceObjectType.LOG:
+ return ResourceKind.LOG;
default:
return undefined;
}
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 = {
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) => {
export const getProcessUrl = (uuid: string) => `/processes/${uuid}`;
+export const getProcessLogUrl = (uuid: string) => `/process-logs/${uuid}`;
+
export const addRouteChangeHandlers = (history: History, store: RootStore) => {
const handler = handleLocationChange(store);
handler(history.location);
history.listen(handler);
};
+ export interface ResourceRouteParams {
+ id: string;
+ }
+
export const matchRootRoute = (route: string) =>
matchPath(route, { path: Routes.ROOT, exact: true });
export const matchFavoritesRoute = (route: string) =>
matchPath(route, { path: Routes.FAVORITES });
- export interface ResourceRouteParams {
- id: string;
- }
+ export const matchTrashRoute = (route: string) =>
+ matchPath(route, { path: Routes.TRASH });
export const matchProjectRoute = (route: string) =>
matchPath<ResourceRouteParams>(route, { path: Routes.PROJECTS });
export const matchProcessRoute = (route: string) =>
matchPath<ResourceRouteParams>(route, { path: Routes.PROCESSES });
+export const matchProcessLogRoute = (route: string) =>
+ matchPath<ResourceRouteParams>(route, { path: Routes.PROCESS_LOGS });
const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
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));
}
};
import * as _ from "lodash";
import { AxiosInstance, AxiosPromise } from "axios";
- import { Resource } from "~/models/resource";
+ import { Resource } from "src/models/resource";
export interface ListArguments {
limit?: number;
export class CommonResourceService<T extends Resource> {
- static mapResponseKeys = (response: any): Promise<any> =>
+ static mapResponseKeys = (response: { data: any }): Promise<any> =>
CommonResourceService.mapKeys(_.camelCase)(response.data)
static mapKeys = (mapFn: (key: string) => string) =>
it("#contents", async () => {
axiosMock
-- .onGet("/groups/1/contents/")
++ .onGet("/groups/1/contents")
.reply(200, {
kind: "kind",
offset: 2,
--- /dev/null
- 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");
+ }
+}
// SPDX-License-Identifier: AGPL-3.0
import { unionize, ofType, UnionOf } from '~/common/unionize';
- import { ContextMenuPosition, ContextMenuResource } from "./context-menu-reducer";
+ import { ContextMenuPosition } from "./context-menu-reducer";
import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
import { Dispatch } from 'redux';
import { RootState } from '~/store/store';
import { getResource } from '../resources/resources';
import { ProjectResource } from '~/models/project';
- import { UserResource } from '../../models/user';
+ import { UserResource } from '~/models/user';
import { isSidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
- import { extractUuidKind, ResourceKind } from '~/models/resource';
- import { matchProcessRoute } from '~/routes/routes';
+ import { extractUuidKind, ResourceKind, TrashableResource } from '~/models/resource';
export const contextMenuActions = unionize({
OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
export type ContextMenuAction = UnionOf<typeof contextMenuActions>;
- export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: { name: string; uuid: string; description?: string; kind: ContextMenuKind; }) =>
+ export type ContextMenuResource = {
+ name: string;
+ uuid: string;
+ ownerUuid: string;
+ description?: string;
+ kind: ResourceKind,
+ menuKind: ContextMenuKind;
+ isTrashed?: boolean;
+ };
+
+ export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource) =>
(dispatch: Dispatch) => {
event.preventDefault();
dispatch(
export const openRootProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
(dispatch: Dispatch, getState: () => RootState) => {
- const userResource = getResource<UserResource>(projectUuid)(getState().resources);
- if (userResource) {
+ const res = getResource<UserResource>(projectUuid)(getState().resources);
+ if (res) {
dispatch<any>(openContextMenu(event, {
name: '',
- uuid: userResource.uuid,
- kind: ContextMenuKind.ROOT_PROJECT
+ uuid: res.uuid,
+ ownerUuid: res.uuid,
+ kind: res.kind,
+ menuKind: ContextMenuKind.ROOT_PROJECT,
+ isTrashed: false
}));
}
};
export const openProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
(dispatch: Dispatch, getState: () => RootState) => {
- const projectResource = getResource<ProjectResource>(projectUuid)(getState().resources);
- if (projectResource) {
+ const res = getResource<ProjectResource>(projectUuid)(getState().resources);
+ if (res) {
dispatch<any>(openContextMenu(event, {
- name: projectResource.name,
- uuid: projectResource.uuid,
- kind: ContextMenuKind.PROJECT
+ name: res.name,
+ uuid: res.uuid,
+ kind: res.kind,
+ menuKind: ContextMenuKind.PROJECT,
+ ownerUuid: res.ownerUuid,
+ isTrashed: res.isTrashed
}));
}
};
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));
};
import { getProjectUrl } from "~/models/project";
import { SidePanelTreeCategory } from '../side-panel-tree/side-panel-tree-actions';
-import { Routes, getProcessUrl } from '~/routes/routes';
+import { Routes, getProcessUrl, getProcessLogUrl } from '~/routes/routes';
export const navigateTo = (uuid: string) =>
async (dispatch: Dispatch) => {
dispatch<any>(navigateToCollection(uuid));
} else if (kind === ResourceKind.CONTAINER_REQUEST) {
dispatch<any>(navigateToProcess(uuid));
- }
+ }
if (uuid === SidePanelTreeCategory.FAVORITES) {
dispatch<any>(navigateToFavorites);
}
export const navigateToFavorites = push(Routes.FAVORITES);
+ export const navigateToTrash = push(Routes.TRASH);
+
export const navigateToProject = compose(push, getProjectUrl);
export const navigateToCollection = compose(push, getCollectionUrl);
export const navigateToProcess = compose(push, getProcessUrl);
+
+export const navigateToProcessLogs = compose(push, getProcessLogUrl);
--- /dev/null
- 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,
+];
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);
dispatch<any>(updateResources([container]));
+ await dispatch<any>(loadSubprocesses(containerRequest.containerUuid));
+ return { containerRequest, container };
}
+ return { containerRequest };
};
-export const loadSubprocesses = (uuid: string) =>
+export const loadSubprocesses = (containerUuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const containerRequests = await dispatch<any>(loadContainerRequests(
- new FilterBuilder().addEqual('requestingContainerUuid', uuid).getFilters()
+ new FilterBuilder().addEqual('requestingContainerUuid', containerUuid).getFilters()
)) as ContainerRequestResource[];
const containerUuids: string[] = containerRequests.reduce((uuids, { containerUuid }) =>
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' &&
const favoritePanelMiddleware = dataExplorerMiddleware(
new FavoritePanelMiddlewareService(services, FAVORITE_PANEL_ID)
);
+ const trashPanelMiddleware = dataExplorerMiddleware(
+ new TrashPanelMiddlewareService(services, TRASH_PANEL_ID)
+ );
const middlewares: Middleware[] = [
routerMiddleware(history),
thunkMiddleware.withExtraArgument(services),
projectPanelMiddleware,
- favoritePanelMiddleware
+ favoritePanelMiddleware,
+ trashPanelMiddleware
];
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
return createStore(rootReducer, enhancer);
dialog: dialogReducer,
favorites: favoritesReducer,
form: formReducer,
+ processLogsPanel: processLogsPanelReducer,
properties: propertiesReducer,
resources: resourcesReducer,
router: routerReducer,
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 = () =>
if (userResource) {
dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
+ dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
dispatch<any>(initSidePanelTree());
if (router.location) {
const match = matchRootRoute(router.location.pathname);
dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
};
+ export const loadTrash = () =>
+ (dispatch: Dispatch) => {
+ dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
+ dispatch<any>(loadTrashPanel());
+ dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.TRASH));
+ };
export const loadProject = (uuid: string) =>
async (dispatch: Dispatch) => {
}
};
+export const loadProcessLog = (uuid: string) =>
+ async (dispatch: Dispatch) => {
+ dispatch<any>(initProcessLogsPanel(uuid));
+ };
+
export const resourceIsNotLoaded = (uuid: string) =>
snackbarActions.OPEN_SNACKBAR({
message: `Resource identified by ${uuid} is not loaded.`
message: 'Could not load user'
});
- const reloadProjectMatchingUuid = (matchingUuids: string[]) =>
+ export const reloadProjectMatchingUuid = (matchingUuids: string[]) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const currentProjectPanelUuid = getProjectPanelCurrentUuid(getState());
if (currentProjectPanelUuid && matchingUuids.some(uuid => uuid === currentProjectPanelUuid)) {
dispatch<any>(loadProject(currentProjectPanelUuid));
}
- };
+ };
import { connect } from "react-redux";
import { RootState } from "~/store/store";
- import { contextMenuActions } from "~/store/context-menu/context-menu-actions";
+ import { contextMenuActions, ContextMenuResource } from "~/store/context-menu/context-menu-actions";
import { ContextMenu as ContextMenuComponent, ContextMenuProps, ContextMenuItem } from "~/components/context-menu/context-menu";
import { createAnchorAt } from "~/components/popover/helpers";
- import { ContextMenuResource } from "~/store/context-menu/context-menu-reducer";
import { ContextMenuActionSet, ContextMenuAction } from "./context-menu-action-set";
import { Dispatch } from "redux";
};
const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet => {
- return resource ? menuActionSets.get(resource.kind) || [] : [];
+ return resource ? menuActionSets.get(resource.menuKind) || [] : [];
};
export enum ContextMenuKind {
PROJECT = "Project",
RESOURCE = "Resource",
FAVORITE = "Favorite",
+ TRASH = "Trash",
COLLECTION_FILES = "CollectionFiles",
COLLECTION_FILES_ITEM = "CollectionFilesItem",
COLLECTION = 'Collection',
COLLECTION_RESOURCE = 'CollectionResource',
- PROCESS = "Process"
+ PROCESS = "Process",
+ PROCESS_LOGS = "ProcessLogs"
}
import { Routes } from '~/routes/routes';
import { SidePanel } from '~/views-components/side-panel/side-panel';
import { ProcessPanel } from '~/views/process-panel/process-panel';
+import { ProcessLogPanel } from '~/views/process-log-panel/process-log-panel';
import { Breadcrumbs } from '~/views-components/breadcrumbs/breadcrumbs';
import { CreateProjectDialog } from '~/views-components/dialog-forms/create-project-dialog';
import { CreateCollectionDialog } from '~/views-components/dialog-forms/create-collection-dialog';
import { UpdateProjectDialog } from '~/views-components/dialog-forms/update-project-dialog';
import { MoveProjectDialog } from '~/views-components/dialog-forms/move-project-dialog';
import { MoveCollectionDialog } from '~/views-components/dialog-forms/move-collection-dialog';
-
import { FilesUploadCollectionDialog } from '~/views-components/dialog-forms/files-upload-collection-dialog';
import { PartialCopyCollectionDialog } from '~/views-components/dialog-forms/partial-copy-collection-dialog';
+ import { TrashPanel } from "~/views/trash-panel/trash-panel";
+
const APP_BAR_HEIGHT = 100;
type CssRules = 'root' | 'appBar' | 'content' | 'contentWrapper';
<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 />}
--- /dev/null
- 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"}');
+ }
+
+}
--- /dev/null
- 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}>));
+ }
+};