//
// 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) => {
defaultIcon: ProjectIcon,
onSetColumns: jest.fn(),
defaultMessages: ['testing'],
+ contextMenuColumn: true
});
page: number;
defaultIcon: IconType;
defaultMessages: string[];
+ contextMenuColumn: boolean;
}
interface DataExplorerActionProps<T> {
</Grid>
</Toolbar>
<DataTable
- columns={[...columns, this.contextMenuColumn]}
+ columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
items={items}
onRowClick={(_, item: T) => onRowClick(item)}
onContextMenu={onContextMenu}
</IconButton>
</Grid>
- contextMenuColumn = {
+ contextMenuColumn: DataColumn<any> = {
name: "Actions",
selected: true,
configurable: false,
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 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';
addMenuActionSet(ContextMenuKind.COLLECTION, collectionActionSet);
addMenuActionSet(ContextMenuKind.COLLECTION_RESOURCE, collectionResourceActionSet);
addMenuActionSet(ContextMenuKind.PROCESS, processActionSet);
+addMenuActionSet(ContextMenuKind.TRASH, trashActionSet);
fetchConfig()
.then((config) => {
//
// SPDX-License-Identifier: AGPL-3.0
-import { Resource, ResourceKind } from "./resource";
+import { ResourceKind, TrashableResource } from "./resource";
-export interface CollectionResource extends Resource {
+export interface CollectionResource extends TrashableResource {
kind: ResourceKind.COLLECTION;
name: string;
description: string;
replicationDesired: number;
replicationConfirmed: number;
replicationConfirmedAt: string;
- trashAt: string;
- deleteAt: string;
- isTrashed: boolean;
}
export const getCollectionUrl = (uuid: string) => {
return `/collections/${uuid}`;
-};
\ No newline at end of file
+};
//
// SPDX-License-Identifier: AGPL-3.0
-import { Resource, ResourceKind } from "./resource";
+import { ResourceKind, TrashableResource } from "./resource";
-export interface GroupResource extends Resource {
+export interface GroupResource extends TrashableResource {
kind: ResourceKind.GROUP;
name: string;
groupClass: GroupClass | null;
description: string;
properties: string;
writeableBy: string[];
- trashAt: string;
- deleteAt: string;
- isTrashed: boolean;
}
export enum GroupClass {
PROJECT = "project"
-}
\ No newline at end of file
+}
//
// SPDX-License-Identifier: AGPL-3.0
-import { GroupResource, GroupClass } from "./group";
+import { GroupClass, GroupResource } from "./group";
export interface ProjectResource extends GroupResource {
groupClass: GroupClass.PROJECT;
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",
PROJECT = "arvados#group",
USER = "arvados#user",
WORKFLOW = "arvados#workflow",
+ NONE = "arvados#none"
}
export enum ResourceObjectType {
//
// SPDX-License-Identifier: AGPL-3.0
-import { GroupResource, GroupClass } from "./group";
+import { GroupClass, GroupResource } from "./group";
import { Resource, ResourceKind } from "./resource";
import { ProjectResource } from "./project";
createdAt: "",
etag: "",
href: "",
- kind: "",
+ kind: ResourceKind.NONE,
modifiedAt: "",
modifiedByClientUuid: "",
modifiedByUserUuid: "",
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, loadProcessLog } from '~/store/workbench/workbench-actions';
import { loadProcessPanel } from '~/store/process-panel/process-panel-actions';
export const Routes = {
COLLECTIONS: `/collections/:id(${RESOURCE_UUID_PATTERN})`,
PROCESSES: `/processes/:id(${RESOURCE_UUID_PATTERN})`,
FAVORITES: '/favorites',
+ TRASH: '/trash',
PROCESS_LOGS: `/process-logs/:id(${RESOURCE_UUID_PATTERN})`
};
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 });
const projectMatch = matchProjectRoute(pathname);
const collectionMatch = matchCollectionRoute(pathname);
const favoriteMatch = matchFavoritesRoute(pathname);
+ const trashMatch = matchTrashRoute(pathname);
const processMatch = matchProcessRoute(pathname);
const processLogMatch = matchProcessLogRoute(pathname);
store.dispatch(loadCollection(collectionMatch.params.id));
} else if (favoriteMatch) {
store.dispatch(loadFavorites());
+ } else if (trashMatch) {
+ store.dispatch(loadTrash());
} else if (processMatch) {
store.dispatch(loadProcessPanel(processMatch.params.id));
} else if (processLogMatch) {
import { UserService } from '../user-service/user-service';
import { GroupResource } from '~/models/group';
import { UserResource } from '~/models/user';
-import { extractUuidObjectType, ResourceObjectType } from "~/models/resource";
+import { extractUuidObjectType, ResourceObjectType, TrashableResource } from "~/models/resource";
export class AncestorService {
constructor(
private userService: UserService
) { }
- async ancestors(uuid: string, rootUuid: string): Promise<Array<UserResource | GroupResource>> {
+ async ancestors(uuid: string, rootUuid: string): Promise<Array<UserResource | GroupResource | TrashableResource>> {
const service = this.getService(extractUuidObjectType(uuid));
if (service) {
const resource = await service.get(uuid);
return undefined;
}
}
-}
\ No newline at end of file
+}
// SPDX-License-Identifier: AGPL-3.0
import * as _ from "lodash";
-import { Resource } from "~/models/resource";
+import { Resource } from "src/models/resource";
export enum OrderDirection { ASC, DESC }
import { CollectionService } from "../collection-service/collection-service";
import { parseKeepManifestText, stringifyKeepManifest } from "./collection-manifest-parser";
import { mapManifestToCollectionFilesTree } from "./collection-manifest-mapper";
-import { CommonResourceService } from "~/common/api/common-resource-service";
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
import * as _ from "lodash";
export class CollectionFilesService {
//
// SPDX-License-Identifier: AGPL-3.0
-import { CommonResourceService } from "~/common/api/common-resource-service";
import { CollectionResource } from "~/models/collection";
import { AxiosInstance } from "axios";
import { CollectionFile, CollectionDirectory } from "~/models/collection-file";
import { mapTreeValues } from "~/models/tree";
import { parseFilesResponse } from "./collection-service-files-response";
import { fileToArrayBuffer } from "~/common/file";
+import { TrashableResourceService } from "~/services/common-service/trashable-resource-service";
export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
-export class CollectionService extends CommonResourceService<CollectionResource> {
+export class CollectionService extends TrashableResourceService<CollectionResource> {
constructor(serverApi: AxiosInstance, private webdavClient: WebDAV, private authService: AuthService) {
super(serverApi, "collections");
}
return this.webdavClient.put(fileURL, fileContent, requestConfig);
}
-
}
import { CommonResourceService } from "./common-resource-service";
import axios, { AxiosInstance } from "axios";
import MockAdapter from "axios-mock-adapter";
-import { Resource } from "~/models/resource";
+import { Resource } from "src/models/resource";
export const mockResourceService = <R extends Resource, C extends CommonResourceService<R>>(Service: new (client: AxiosInstance) => C) => {
const axiosInstance = axios.create();
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;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as _ from "lodash";
+import { AxiosInstance } from "axios";
+import { TrashableResource } from "src/models/resource";
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
+
+export class TrashableResourceService<T extends TrashableResource> extends CommonResourceService<T> {
+
+ constructor(serverApi: AxiosInstance, resourceType: string) {
+ super(serverApi, resourceType);
+ }
+
+ trash(uuid: string): Promise<T> {
+ return this.serverApi
+ .post(this.resourceType + `${uuid}/trash`)
+ .then(CommonResourceService.mapResponseKeys);
+ }
+
+ untrash(uuid: string): Promise<T> {
+ const params = {
+ ensure_unique_name: true
+ };
+ return this.serverApi
+ .post(this.resourceType + `${uuid}/untrash`, {
+ params: CommonResourceService.mapKeys(_.snakeCase)(params)
+ })
+ .then(CommonResourceService.mapResponseKeys);
+ }
+}
//
// SPDX-License-Identifier: AGPL-3.0
-import { CommonResourceService } from "~/common/api/common-resource-service";
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
import { AxiosInstance } from "axios";
import { ContainerRequestResource } from '../../models/container-request';
//
// SPDX-License-Identifier: AGPL-3.0
-import { CommonResourceService } from "~/common/api/common-resource-service";
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
import { AxiosInstance } from "axios";
import { ContainerResource } from '../../models/container';
import { GroupsService } from "../groups-service/groups-service";
import { FavoriteService } from "./favorite-service";
import { LinkClass } from "~/models/link";
-import { mockResourceService } from "~/common/api/common-resource-service.test";
-import { FilterBuilder } from "~/common/api/filter-builder";
+import { mockResourceService } from "~/services/common-service/common-resource-service.test";
+import { FilterBuilder } from "~/services/api/filter-builder";
describe("FavoriteService", () => {
import { LinkService } from "../link-service/link-service";
import { GroupsService, GroupContentsResource } from "../groups-service/groups-service";
import { LinkClass } from "~/models/link";
-import { FilterBuilder, joinFilters } from "~/common/api/filter-builder";
-import { ListResults } from "~/common/api/common-resource-service";
+import { FilterBuilder, joinFilters } from "~/services/api/filter-builder";
+import { ListResults } from "~/services/common-service/common-resource-service";
export interface FavoriteListArguments {
limit?: number;
it("#contents", async () => {
axiosMock
- .onGet("/groups/1/contents/")
+ .onGet("/groups/1/contents")
.reply(200, {
kind: "kind",
offset: 2,
// SPDX-License-Identifier: AGPL-3.0
import * as _ from "lodash";
-import { CommonResourceService, ListResults } from "~/common/api/common-resource-service";
+import { CommonResourceService, ListResults } from "~/services/common-service/common-resource-service";
import { AxiosInstance } from "axios";
-import { GroupResource } from "~/models/group";
import { CollectionResource } from "~/models/collection";
import { ProjectResource } from "~/models/project";
import { ProcessResource } from "~/models/process";
+import { TrashableResource } from "~/models/resource";
+import { TrashableResourceService } from "~/services/common-service/trashable-resource-service";
export interface ContentsArguments {
limit?: number;
order?: string;
filters?: string;
recursive?: boolean;
+ includeTrash?: boolean;
}
export type GroupContentsResource =
ProjectResource |
ProcessResource;
-export class GroupsService<T extends GroupResource = GroupResource> extends CommonResourceService<T> {
+export class GroupsService<T extends TrashableResource = TrashableResource> extends TrashableResourceService<T> {
constructor(serverApi: AxiosInstance) {
super(serverApi, "groups");
order: order ? order : undefined
};
return this.serverApi
- .get(this.resourceType + `${uuid}/contents/`, {
+ .get(this.resourceType + `${uuid}/contents`, {
params: CommonResourceService.mapKeys(_.snakeCase)(params)
})
.then(CommonResourceService.mapResponseKeys);
//\r
// SPDX-License-Identifier: AGPL-3.0\r
\r
-import { CommonResourceService } from "~/common/api/common-resource-service";\r
+import { CommonResourceService } from "~/services/common-service/common-resource-service";\r
import { AxiosInstance } from "axios";\r
import { KeepResource } from "~/models/keep";\r
\r
//
// SPDX-License-Identifier: AGPL-3.0
-import { CommonResourceService } from "~/common/api/common-resource-service";
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
import { LinkResource } from "~/models/link";
import { AxiosInstance } from "axios";
//
// 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<LogResource> {
constructor(serverApi: AxiosInstance) {
import axios from "axios";
import { ProjectService } from "./project-service";
-import { FilterBuilder } from "~/common/api/filter-builder";
+import { FilterBuilder } from "~/services/api/filter-builder";
describe("CommonResourceService", () => {
const axiosInstance = axios.create();
import { GroupsService } from "../groups-service/groups-service";
import { ProjectResource } from "~/models/project";
import { GroupClass } from "~/models/group";
-import { ListArguments } from "~/common/api/common-resource-service";
-import { FilterBuilder, joinFilters } from "~/common/api/filter-builder";
+import { ListArguments } from "~/services/common-service/common-resource-service";
+import { FilterBuilder, joinFilters } from "~/services/api/filter-builder";
export class ProjectService extends GroupsService<ProjectResource> {
import { LinkService } from "../link-service/link-service";
import { LinkClass } from "~/models/link";
-import { FilterBuilder } from "~/common/api/filter-builder";
+import { FilterBuilder } from "~/services/api/filter-builder";
import { TagTailType, TagResource } from "~/models/tag";
-import { OrderBuilder } from "~/common/api/order-builder";
+import { OrderBuilder } from "~/services/api/order-builder";
export class TagService {
// SPDX-License-Identifier: AGPL-3.0
import { AxiosInstance } from "axios";
-import { CommonResourceService } from "~/common/api/common-resource-service";
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
import { UserResource } from "~/models/user";
export class UserService extends CommonResourceService<UserResource> {
constructor(serverApi: AxiosInstance) {
super(serverApi, "users");
}
-}
\ No newline at end of file
+}
import { getNodeValue } from "~/models/tree";
import { filterCollectionFilesBySelection } from './collection-panel-files-state';
import { startSubmit, initialize, stopSubmit, reset } from 'redux-form';
-import { getCommonResourceServiceError, CommonResourceServiceError } from "~/common/api/common-resource-service";
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
import { getDialog } from "~/store/dialog/dialog-reducer";
import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
import { RootState } from '~/store/store';
import { ServiceRepository } from '~/services/services';
-import { getCommonResourceServiceError, CommonResourceServiceError } from '~/common/api/common-resource-service';
+import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
export const COLLECTION_COPY_FORM_NAME = 'collectionCopyFormName';
}
return ;
}
- };
\ No newline at end of file
+ };
import { RootState } from '~/store/store';
import { dialogActions } from "~/store/dialog/dialog-actions";
import { ServiceRepository } from '~/services/services';
-import { getCommonResourceServiceError, CommonResourceServiceError } from "~/common/api/common-resource-service";
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
import { uploadCollectionFiles } from './collection-upload-actions';
import { fileUploaderActions } from '~/store/file-uploader/file-uploader-actions';
import { startSubmit, stopSubmit, initialize } from 'redux-form';
import { ServiceRepository } from '~/services/services';
import { RootState } from '~/store/store';
-import { getCommonResourceServiceError, CommonResourceServiceError } from "~/common/api/common-resource-service";
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
import { snackbarActions } from '~/store/snackbar/snackbar-actions';
import { projectPanelActions } from '~/store/project-panel/project-panel-action';
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
import { ServiceRepository } from '~/services/services';
import { filterCollectionFilesBySelection } from '../collection-panel/collection-panel-files/collection-panel-files-state';
import { snackbarActions } from '~/store/snackbar/snackbar-actions';
-import { getCommonResourceServiceError, CommonResourceServiceError } from '~/common/api/common-resource-service';
+import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
export const COLLECTION_PARTIAL_COPY_FORM_NAME = 'COLLECTION_PARTIAL_COPY_DIALOG';
}
}
}
- };
\ No newline at end of file
+ };
import { initialize, startSubmit, stopSubmit } from 'redux-form';
import { RootState } from "~/store/store";
import { collectionPanelActions } from "~/store/collection-panel/collection-panel-action";
-import { loadDetailsPanel } from "~/store/details-panel/details-panel-action";
import { dialogActions } from "~/store/dialog/dialog-actions";
-import { dataExplorerActions } from "~/store/data-explorer/data-explorer-action";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
-import { ContextMenuResource } from '~/store/context-menu/context-menu-reducer';
-import { PROJECT_PANEL_ID } from "~/views/project-panel/project-panel";
-import { getCommonResourceServiceError, CommonResourceServiceError } from "~/common/api/common-resource-service";
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
import { ServiceRepository } from "~/services/services";
import { CollectionResource } from '~/models/collection';
+import { ContextMenuResource } from "~/store/context-menu/context-menu-actions";
export interface CollectionUpdateFormDialogData {
uuid: string;
}
return;
}
- };
\ No newline at end of file
+ };
// 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 { 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
}));
}
};
// 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));
};
//
// SPDX-License-Identifier: AGPL-3.0
-import { contextMenuActions, ContextMenuAction } from "./context-menu-actions";
+import { contextMenuActions, ContextMenuAction, ContextMenuResource } from "./context-menu-actions";
export interface ContextMenuState {
open: boolean;
y: number;
}
-export interface ContextMenuResource {
- uuid: string;
- kind: string;
- name: string;
- description?: string;
-}
-
const initialState = {
open: false,
position: { x: 0, y: 0 }
import { DataColumns } from "~/components/data-table/data-table";
import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters";
import { DataExplorer } from './data-explorer-reducer';
-import { ListArguments, ListResults } from '~/common/api/common-resource-service';
+import { ListResults } from '~/services/common-service/common-resource-service';
export abstract class DataExplorerMiddlewareService {
protected readonly id: string;
itemsAvailable,
page: Math.floor(offset / limit),
rowsPerPage: limit
-});
\ No newline at end of file
+});
import { DataColumns } from "~/components/data-table/data-table";
import { ServiceRepository } from "~/services/services";
import { SortDirection } from "~/components/data-table/data-column";
-import { FilterBuilder } from "~/common/api/filter-builder";
+import { FilterBuilder } from "~/services/api/filter-builder";
import { updateFavorites } from "../favorites/favorites-actions";
import { favoritePanelActions } from "./favorite-panel-action";
import { Dispatch, MiddlewareAPI } from "redux";
-import { OrderBuilder, OrderDirection } from "~/common/api/order-builder";
+import { OrderBuilder, OrderDirection } from "~/services/api/order-builder";
import { LinkResource } from "~/models/link";
import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
import { resourcesActions } from "~/store/resources/resources-actions";
export const navigateToFavorites = push(Routes.FAVORITES);
+export const navigateToTrash = push(Routes.TRASH);
+
export const navigateToProject = compose(push, getProjectUrl);
export const navigateToCollection = compose(push, getCollectionUrl);
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<{}>(),
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';
import { DataColumns } from "~/components/data-table/data-table";
import { ServiceRepository } from "~/services/services";
import { SortDirection } from "~/components/data-table/data-column";
-import { OrderBuilder, OrderDirection } from "~/common/api/order-builder";
-import { FilterBuilder } from "~/common/api/filter-builder";
+import { OrderBuilder, OrderDirection } from "~/services/api/order-builder";
+import { FilterBuilder } from "~/services/api/filter-builder";
import { GroupContentsResourcePrefix, GroupContentsResource } from "~/services/groups-service/groups-service";
import { updateFavorites } from "../favorites/favorites-actions";
import { projectPanelActions, PROJECT_PANEL_CURRENT_UUID } from './project-panel-action';
import { getProperty } from "~/store/properties/properties";
import { snackbarActions } from '../snackbar/snackbar-actions';
import { DataExplorer, getDataExplorer } from '../data-explorer/data-explorer-reducer';
-import { ListResults } from '~/common/api/common-resource-service';
+import { ListResults } from '~/services/common-service/common-resource-service';
export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
import { reset, startSubmit, stopSubmit, initialize } from 'redux-form';
import { RootState } from '~/store/store';
import { dialogActions } from "~/store/dialog/dialog-actions";
-import { getCommonResourceServiceError, CommonResourceServiceError } from '~/common/api/common-resource-service';
+import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
import { ProjectResource } from '~/models/project';
import { ServiceRepository } from '~/services/services';
import { startSubmit, stopSubmit, initialize } from 'redux-form';
import { ServiceRepository } from '~/services/services';
import { RootState } from '~/store/store';
-import { getCommonResourceServiceError, CommonResourceServiceError } from "~/common/api/common-resource-service";
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
import { initialize, startSubmit, stopSubmit } from 'redux-form';
import { RootState } from "~/store/store";
import { dialogActions } from "~/store/dialog/dialog-actions";
-import { ContextMenuResource } from '~/store/context-menu/context-menu-reducer';
-import { getCommonResourceServiceError, CommonResourceServiceError } from "~/common/api/common-resource-service";
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
import { ServiceRepository } from "~/services/services";
import { ProjectResource } from '~/models/project';
+import { ContextMenuResource } from "~/store/context-menu/context-menu-actions";
export interface ProjectUpdateFormDialogData {
uuid: string;
}
return ;
}
- };
\ No newline at end of file
+ };
import { createTreePickerNode, TreePickerNode } from '~/store/tree-picker/tree-picker';
import { RootState } from '../store';
import { ServiceRepository } from '~/services/services';
-import { FilterBuilder } from '~/common/api/filter-builder';
+import { FilterBuilder } from '~/services/api/filter-builder';
import { resourcesActions } from '../resources/resources-actions';
import { getTreePicker, TreePicker } from '../tree-picker/tree-picker';
import { TreeItemStatus } from "~/components/tree/tree";
import { Dispatch } from 'redux';
import { isSidePanelTreeCategory, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
-import { navigateToFavorites, navigateTo } from '../navigation/navigation-action';
+import { navigateToFavorites, navigateTo, navigateToTrash } from '../navigation/navigation-action';
import { snackbarActions } from '~/store/snackbar/snackbar-actions';
export const navigateFromSidePanel = (id: string) =>
switch (id) {
case SidePanelTreeCategory.FAVORITES:
return navigateToFavorites;
+ case SidePanelTreeCategory.TRASH:
+ return navigateToTrash;
default:
return sidePanelTreeCategoryNotAvailable(id);
}
snackbarActions.OPEN_SNACKBAR({
message: `${id} not available`,
hideDuration: 3000,
- });
\ No newline at end of file
+ });
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';
import { processPanelReducer } from '~/store/process-panel/process-panel-reducer';
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);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { bindDataExplorerActions } from "../data-explorer/data-explorer-action";
+import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
+
+export const TRASH_PANEL_ID = "trashPanel";
+export const trashPanelActions = bindDataExplorerActions(TRASH_PANEL_ID);
+
+export const loadTrashPanel = () => trashPanelActions.REQUEST_ITEMS();
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import {
+ DataExplorerMiddlewareService, dataExplorerToListParams,
+ listResultsToDataExplorerItemsMeta
+} from "../data-explorer/data-explorer-middleware-service";
+import { RootState } from "../store";
+import { DataColumns } from "~/components/data-table/data-table";
+import { ServiceRepository } from "~/services/services";
+import { SortDirection } from "~/components/data-table/data-column";
+import { FilterBuilder } from "~/services/api/filter-builder";
+import { trashPanelActions } from "./trash-panel-action";
+import { Dispatch, MiddlewareAPI } from "redux";
+import { OrderBuilder, OrderDirection } from "~/services/api/order-builder";
+import { GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
+import { TrashPanelColumnNames, TrashPanelFilter } from "~/views/trash-panel/trash-panel";
+import { ProjectResource } from "~/models/project";
+import { ProjectPanelColumnNames } from "~/views/project-panel/project-panel";
+import { updateFavorites } from "~/store/favorites/favorites-actions";
+import { TrashableResource } from "~/models/resource";
+import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { updateResources } from "~/store/resources/resources-actions";
+
+export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
+ constructor(private services: ServiceRepository, id: string) {
+ super(id);
+ }
+
+ async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+ const dataExplorer = api.getState().dataExplorer[this.getId()];
+ const columns = dataExplorer.columns as DataColumns<string, TrashPanelFilter>;
+ const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+ const typeFilters = this.getColumnFilters(columns, TrashPanelColumnNames.TYPE);
+
+ const order = new OrderBuilder<ProjectResource>();
+
+ if (sortColumn) {
+ const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
+ ? OrderDirection.ASC
+ : OrderDirection.DESC;
+
+ const columnName = sortColumn && sortColumn.name === ProjectPanelColumnNames.NAME ? "name" : "createdAt";
+ order
+ .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.COLLECTION)
+ .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROJECT);
+ }
+
+ try {
+ const userUuid = this.services.authService.getUuid()!;
+ const listResults = await this.services.groupsService
+ .contents(userUuid, {
+ ...dataExplorerToListParams(dataExplorer),
+ order: order.getOrder(),
+ filters: new FilterBuilder()
+ .addIsA("uuid", typeFilters.map(f => f.type))
+ .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
+ .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
+ .getFilters(),
+ recursive: true,
+ includeTrash: true
+ });
+
+ const items = listResults.items
+ .filter(it => (it as TrashableResource).isTrashed)
+ .map(it => it.uuid);
+
+ api.dispatch(trashPanelActions.SET_ITEMS({
+ ...listResultsToDataExplorerItemsMeta(listResults),
+ items
+ }));
+ api.dispatch<any>(updateFavorites(items));
+ api.dispatch(updateResources(listResults.items));
+ } catch (e) {
+ api.dispatch(couldNotFetchTrashContents());
+ }
+ }
+}
+
+const couldNotFetchTrashContents = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Could not fetch trash contents.'
+ });
--- /dev/null
+// 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 { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { trashPanelActions } from "~/store/trash-panel/trash-panel-action";
+import { activateSidePanelTreeItem, loadSidePanelTreeProjects } from "~/store/side-panel-tree/side-panel-tree-actions";
+import { projectPanelActions } from "~/store/project-panel/project-panel-action";
+import { ResourceKind, TrashableResource } from "~/models/resource";
+
+export const toggleProjectTrashed = (uuid: string, ownerUuid: string, isTrashed: boolean) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+ if (isTrashed) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash..." }));
+ await services.groupsService.untrash(uuid);
+ dispatch<any>(activateSidePanelTreeItem(uuid));
+ dispatch(trashPanelActions.REQUEST_ITEMS());
+ dispatch(snackbarActions.CLOSE_SNACKBAR());
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Restored from trash",
+ hideDuration: 2000
+ }));
+ } else {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash..." }));
+ await services.groupsService.trash(uuid);
+ dispatch<any>(loadSidePanelTreeProjects(ownerUuid));
+ dispatch(snackbarActions.CLOSE_SNACKBAR());
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Added to trash",
+ hideDuration: 2000
+ }));
+ }
+ };
+
+export const toggleCollectionTrashed = (uuid: string, isTrashed: boolean) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+ if (isTrashed) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash..." }));
+ await services.collectionService.untrash(uuid);
+ dispatch(trashPanelActions.REQUEST_ITEMS());
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Restored from trash",
+ hideDuration: 2000
+ }));
+ } else {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash..." }));
+ await services.collectionService.trash(uuid);
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Added to trash",
+ hideDuration: 2000
+ }));
+ }
+ };
+
+export const toggleTrashed = (kind: ResourceKind, uuid: string, ownerUuid: string, isTrashed: boolean) =>
+ (dispatch: Dispatch) => {
+ if (kind === ResourceKind.PROJECT) {
+ dispatch<any>(toggleProjectTrashed(uuid, ownerUuid, isTrashed!!));
+ } else if (kind === ResourceKind.COLLECTION) {
+ dispatch<any>(toggleCollectionTrashed(uuid, isTrashed!!));
+ }
+ };
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';
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) => {
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));
}
- };
\ No newline at end of file
+ };
// SPDX-License-Identifier: AGPL-3.0
import { connect } from "react-redux";
-import { CollectionPanelFiles as Component, CollectionPanelFilesProps } from "~/components/collection-panel-files/collection-panel-files";
+import {
+ CollectionPanelFiles as Component,
+ CollectionPanelFilesProps
+} from "~/components/collection-panel-files/collection-panel-files";
import { RootState } from "~/store/store";
-import { TreeItemStatus, TreeItem } from "~/components/tree/tree";
-import { CollectionPanelFilesState, CollectionPanelDirectory, CollectionPanelFile } from "~/store/collection-panel/collection-panel-files/collection-panel-files-state";
+import { TreeItem, TreeItemStatus } from "~/components/tree/tree";
+import {
+ CollectionPanelDirectory,
+ CollectionPanelFile,
+ CollectionPanelFilesState
+} from "~/store/collection-panel/collection-panel-files/collection-panel-files-state";
import { FileTreeData } from "~/components/file-tree/file-tree-data";
import { Dispatch } from "redux";
import { collectionPanelFilesAction } from "~/store/collection-panel/collection-panel-files/collection-panel-files-actions";
import { ContextMenuKind } from "../context-menu/context-menu";
-import { Tree, getNodeChildrenIds, getNode } from "~/models/tree";
+import { getNode, getNodeChildrenIds, Tree } from "~/models/tree";
import { CollectionFileType } from "~/models/collection-file";
import { openContextMenu } from '~/store/context-menu/context-menu-actions';
import { openUploadCollectionFilesDialog } from '~/store/collections/collection-upload-actions';
+import { ResourceKind } from "~/models/resource";
const memoizedMapStateToProps = () => {
let prevState: CollectionPanelFilesState;
dispatch(collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: item.id }));
},
onItemMenuOpen: (event, item) => {
- dispatch<any>(openContextMenu(event, { kind: ContextMenuKind.COLLECTION_FILES_ITEM, name: item.data.name, uuid: item.id }));
+ dispatch<any>(openContextMenu(event, { menuKind: ContextMenuKind.COLLECTION_FILES_ITEM, kind: ResourceKind.COLLECTION, name: item.data.name, uuid: item.id, ownerUuid: '' }));
},
onOptionsMenuOpen: (event) => {
- dispatch<any>(openContextMenu(event, { kind: ContextMenuKind.COLLECTION_FILES, name: '', uuid: '' }));
+ dispatch<any>(openContextMenu(event, { menuKind: ContextMenuKind.COLLECTION_FILES, kind: ResourceKind.COLLECTION, name: '', uuid: '', ownerUuid: '' }));
},
});
import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
import { openMoveCollectionDialog } from '~/store/collections/collection-move-actions';
import { openCollectionCopyDialog } from "~/store/collections/collection-copy-actions";
+import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
+import { toggleCollectionTrashed } from "~/store/trash/trash-actions";
export const collectionActionSet: ContextMenuActionSet = [[
{
});
}
},
+ {
+ component: ToggleTrashAction,
+ execute: (dispatch, resource) => {
+ dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
+ }
+ },
{
icon: CopyIcon,
name: "Copy to project",
import { ContextMenuActionSet } from "../context-menu-action-set";
import { ToggleFavoriteAction } from "../actions/favorite-action";
+import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
import { toggleFavorite } from "~/store/favorites/favorites-actions";
import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, RemoveIcon } from "~/components/icon/icon";
import { openCollectionUpdateDialog } from "~/store/collections/collection-update-actions";
import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
import { openMoveCollectionDialog } from '~/store/collections/collection-move-actions';
import { openCollectionCopyDialog } from '~/store/collections/collection-copy-actions';
+import { toggleCollectionTrashed } from "~/store/trash/trash-actions";
export const collectionResourceActionSet: ContextMenuActionSet = [[
{
});
}
},
+ {
+ component: ToggleTrashAction,
+ execute: (dispatch, resource) => {
+ dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
+ }
+ },
{
icon: CopyIcon,
name: "Copy to project",
import { openMoveProjectDialog } from '~/store/projects/project-move-actions';
import { openProjectCreateDialog } from '~/store/projects/project-create-actions';
import { openProjectUpdateDialog } from '~/store/projects/project-update-actions';
+import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
+import { toggleProjectTrashed } from "~/store/trash/trash-actions";
export const projectActionSet: ContextMenuActionSet = [[
{
});
}
},
+ {
+ component: ToggleTrashAction,
+ execute: (dispatch, resource) => {
+ dispatch<any>(toggleProjectTrashed(resource.uuid, resource.ownerUuid, resource.isTrashed!!));
+ }
+ },
{
icon: MoveToIcon,
name: "Move to",
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
+import { toggleTrashed } from "~/store/trash/trash-actions";
+
+export const trashActionSet: ContextMenuActionSet = [[
+ {
+ component: ToggleTrashAction,
+ execute: (dispatch, resource) => {
+ dispatch<any>(toggleTrashed(resource.kind, resource.uuid, resource.ownerUuid, resource.isTrashed!!));
+ }
+ },
+]];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { ListItemIcon, ListItemText, ListItem } from "@material-ui/core";
+import { RestoreFromTrashIcon, TrashIcon } from "~/components/icon/icon";
+import { connect } from "react-redux";
+import { RootState } from "~/store/store";
+
+const mapStateToProps = (state: RootState, props: { onClick: () => {} }) => ({
+ isTrashed: state.contextMenu.resource && state.contextMenu.resource.isTrashed,
+ onClick: props.onClick
+});
+
+export const ToggleTrashAction = connect(mapStateToProps)((props: { isTrashed?: boolean, onClick: () => void }) =>
+ <ListItem button
+ onClick={props.onClick}>
+ <ListItemIcon>
+ {props.isTrashed
+ ? <RestoreFromTrashIcon/>
+ : <TrashIcon/>}
+ </ListItemIcon>
+ <ListItemText style={{ textDecoration: 'none' }}>
+ {props.isTrashed ? "Restore" : "Move to trash"}
+ </ListItemText>
+ </ListItem >);
import { Dispatch } from "redux";
import { ContextMenuItem } from "~/components/context-menu/context-menu";
-import { ContextMenuResource } from "~/store/context-menu/context-menu-reducer";
+import { ContextMenuResource } from "~/store/context-menu/context-menu-actions";
export interface ContextMenuAction extends ContextMenuItem {
execute(dispatch: Dispatch, resource: ContextMenuResource): void;
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',
import * as React from 'react';
import { Grid, Typography } from '@material-ui/core';
import { FavoriteStar } from '../favorite-star/favorite-star';
-import { ResourceKind } from '~/models/resource';
+import { ResourceKind, TrashableResource } from '~/models/resource';
import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon } from '~/components/icon/icon';
import { formatDate, formatFileSize } from '~/common/formatters';
import { resourceLabel } from '~/common/labels';
import { connect } from 'react-redux';
import { RootState } from '~/store/store';
-import { getResource } from '../../store/resources/resources';
+import { getResource } from '~/store/resources/resources';
import { GroupContentsResource } from '~/services/groups-service/groups-service';
import { ProcessResource } from '~/models/process';
export const ResourceName = connect(
(state: RootState, props: { uuid: string }) => {
- const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
return resource || { name: '', uuid: '', kind: '' };
})(renderName);
}
};
-export const renderDate = (date: string) => {
+export const renderDate = (date?: string) => {
return <Typography noWrap>{formatDate(date)}</Typography>;
};
export const ResourceLastModifiedDate = connect(
(state: RootState, props: { uuid: string }) => {
- const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
return { date: resource ? resource.modifiedAt : '' };
})((props: { date: string }) => renderDate(props.date));
+export const ResourceTrashDate = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource<TrashableResource>(props.uuid)(state.resources);
+ return { date: resource ? resource.trashAt : '' };
+ })((props: { date: string }) => renderDate(props.date));
+
+export const ResourceDeleteDate = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource<TrashableResource>(props.uuid)(state.resources);
+ return { date: resource ? resource.deleteAt : '' };
+ })((props: { date: string }) => renderDate(props.date));
+
export const renderFileSize = (fileSize?: number) =>
<Typography noWrap>
{formatFileSize(fileSize)}
export const ResourceFileSize = connect(
(state: RootState, props: { uuid: string }) => {
- const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
return {};
})((props: { fileSize?: number }) => renderFileSize(props.fileSize));
export const ResourceOwner = connect(
(state: RootState, props: { uuid: string }) => {
- const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
return { owner: resource ? resource.ownerUuid : '' };
})((props: { owner: string }) => renderOwner(props.owner));
export const ResourceType = connect(
(state: RootState, props: { uuid: string }) => {
- const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
return { type: resource ? resource.kind : '' };
})((props: { type: string }) => renderType(props.type));
export const ProcessStatus = connect(
(state: RootState, props: { uuid: string }) => {
- const resource = getResource(props.uuid)(state.resources) as ProcessResource | undefined;
+ const resource = getResource<ProcessResource>(props.uuid)(state.resources);
return { status: resource ? resource.state : '-' };
})((props: { status: string }) => renderType(props.status));
import { createTreePickerNode } from "~/store/tree-picker/tree-picker";
import { RootState } from "~/store/store";
import { ServiceRepository } from "~/services/services";
-import { FilterBuilder } from "~/common/api/filter-builder";
+import { FilterBuilder } from "~/services/api/filter-builder";
import { WrappedFieldProps } from 'redux-form';
type ProjectTreePickerProps = Pick<TreePickerProps, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen'>;
import { deleteCollectionTag } from '~/store/collection-panel/collection-panel-action';
import { snackbarActions } from '~/store/snackbar/snackbar-actions';
import { getResource } from '~/store/resources/resources';
-import { contextMenuActions, openContextMenu } from '~/store/context-menu/context-menu-actions';
+import { openContextMenu } from '~/store/context-menu/context-menu-actions';
import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'label' | 'value';
};
})(
class extends React.Component<CollectionPanelProps> {
-
render() {
const { classes, item, tags } = this.props;
return <div>
}
handleContextMenu = (event: React.MouseEvent<any>) => {
- const { uuid, name, description } = this.props.item;
+ const { uuid, ownerUuid, name, description, kind } = this.props.item;
const resource = {
uuid,
+ ownerUuid,
name,
description,
- kind: ContextMenuKind.COLLECTION
+ kind,
+ menuKind: ContextMenuKind.COLLECTION
};
this.props.dispatch<any>(openContextMenu(event, resource));
}
hideDuration: 2000
}));
}
-
}
)
);
import * as React from 'react';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
-import { DispatchProp, connect } from 'react-redux';
+import { connect, DispatchProp } from 'react-redux';
import { DataColumns } from '~/components/data-table/data-table';
import { RouteComponentProps } from 'react-router';
import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
-import { ContainerRequestState } from '~/models/container-request';
import { SortDirection } from '~/components/data-table/data-column';
import { ResourceKind } from '~/models/resource';
import { resourceLabel } from '~/common/labels';
import { ArvadosTheme } from '~/common/custom-theme';
import { FAVORITE_PANEL_ID } from "~/store/favorite-panel/favorite-panel-action";
-import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner, ResourceName } from '~/views-components/data-explorer/renderers';
+import {
+ ProcessStatus,
+ ResourceFileSize,
+ ResourceLastModifiedDate,
+ ResourceName,
+ ResourceOwner,
+ ResourceType
+} from '~/views-components/data-explorer/renderers';
import { FavoriteIcon } from '~/components/icon/icon';
import { Dispatch } from 'redux';
-import { contextMenuActions, openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
-import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
-import { loadDetailsPanel } from '../../store/details-panel/details-panel-action';
+import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
+import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
import { navigateTo } from '~/store/navigation/navigation-action';
+import { ContainerRequestState } from "~/models/container-request";
type CssRules = "toolbar" | "button";
onContextMenu: (event, resourceUuid) => {
const kind = resourceKindToContextMenuKind(resourceUuid);
if (kind) {
- dispatch<any>(openContextMenu(event, { name: '', uuid: resourceUuid, kind }));
+ dispatch<any>(openContextMenu(event, {
+ name: '',
+ uuid: resourceUuid,
+ ownerUuid: '',
+ kind: ResourceKind.NONE,
+ menuKind: kind
+ }));
}
},
onDialogOpen: (ownerUuid: string) => { return; },
onRowDoubleClick={this.props.onItemDoubleClick}
onContextMenu={this.props.onContextMenu}
defaultIcon={FavoriteIcon}
- defaultMessages={['Your favorites list is empty.']} />;
+ defaultMessages={['Your favorites list is empty.']}
+ contextMenuColumn={true}/>;
}
}
)
import { ResourceName } from '~/views-components/data-explorer/renderers';
import { ResourcesState, getResource } from '~/store/resources/resources';
import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
-import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
-import { contextMenuActions, resourceKindToContextMenuKind, openContextMenu } from '~/store/context-menu/context-menu-actions';
-import { CollectionResource } from '~/models/collection';
+import { resourceKindToContextMenuKind, openContextMenu } from '~/store/context-menu/context-menu-actions';
import { ProjectResource } from '~/models/project';
import { navigateTo } from '~/store/navigation/navigation-action';
import { getProperty } from '~/store/properties/properties';
onRowDoubleClick={this.handleRowDoubleClick}
onContextMenu={this.handleContextMenu}
defaultIcon={ProjectIcon}
- defaultMessages={['Your project is empty.', 'Please create a project or create a collection and upload a data.']} />
+ defaultMessages={['Your project is empty.', 'Please create a project or create a collection and upload a data.']}
+ contextMenuColumn={true}/>
</div>;
}
}
handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
- const kind = resourceKindToContextMenuKind(resourceUuid);
- if (kind) {
- this.props.dispatch<any>(openContextMenu(event, { name: '', uuid: resourceUuid, kind }));
+ const menuKind = resourceKindToContextMenuKind(resourceUuid);
+ const resource = getResource<ProjectResource>(resourceUuid)(this.props.resources);
+ if (menuKind && resource) {
+ this.props.dispatch<any>(openContextMenu(event, {
+ name: resource.name,
+ uuid: resource.uuid,
+ ownerUuid: resource.ownerUuid,
+ isTrashed: resource.isTrashed,
+ kind: resource.kind,
+ menuKind
+ }));
}
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { IconButton, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
+import { connect, DispatchProp } from 'react-redux';
+import { DataColumns } from '~/components/data-table/data-table';
+import { RootState } from '~/store/store';
+import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
+import { SortDirection } from '~/components/data-table/data-column';
+import { ResourceKind, TrashableResource } from '~/models/resource';
+import { resourceLabel } from '~/common/labels';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { RestoreFromTrashIcon, TrashIcon } from '~/components/icon/icon';
+import { TRASH_PANEL_ID } from "~/store/trash-panel/trash-panel-action";
+import { getProperty } from "~/store/properties/properties";
+import { PROJECT_PANEL_CURRENT_UUID } from "~/store/project-panel/project-panel-action";
+import { openContextMenu } from "~/store/context-menu/context-menu-actions";
+import { getResource, ResourcesState } from "~/store/resources/resources";
+import {
+ ResourceDeleteDate,
+ ResourceFileSize,
+ ResourceName,
+ ResourceTrashDate,
+ ResourceType
+} from "~/views-components/data-explorer/renderers";
+import { navigateTo } from "~/store/navigation/navigation-action";
+import { loadDetailsPanel } from "~/store/details-panel/details-panel-action";
+import { toggleCollectionTrashed, toggleProjectTrashed, toggleTrashed } from "~/store/trash/trash-actions";
+import { ContextMenuKind } from "~/views-components/context-menu/context-menu";
+import { Dispatch } from "redux";
+
+type CssRules = "toolbar" | "button";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ toolbar: {
+ paddingBottom: theme.spacing.unit * 3,
+ textAlign: "right"
+ },
+ button: {
+ marginLeft: theme.spacing.unit
+ },
+});
+
+export enum TrashPanelColumnNames {
+ NAME = "Name",
+ TYPE = "Type",
+ FILE_SIZE = "File size",
+ TRASHED_DATE = "Trashed date",
+ TO_BE_DELETED = "To be deleted"
+}
+
+export interface TrashPanelFilter extends DataTableFilterItem {
+ type: ResourceKind;
+}
+
+export const ResourceRestore =
+ connect((state: RootState, props: { uuid: string, dispatch?: Dispatch<any> }) => {
+ const resource = getResource<TrashableResource>(props.uuid)(state.resources);
+ return { resource, dispatch: props.dispatch };
+ })((props: { resource?: TrashableResource, dispatch?: Dispatch<any> }) =>
+ <IconButton onClick={() => {
+ if (props.resource && props.dispatch) {
+ props.dispatch(toggleTrashed(
+ props.resource.kind,
+ props.resource.uuid,
+ props.resource.ownerUuid,
+ props.resource.isTrashed
+ ));
+ }
+ }}>
+ <RestoreFromTrashIcon/>
+ </IconButton>
+ );
+
+export const trashPanelColumns: DataColumns<string, TrashPanelFilter> = [
+ {
+ name: TrashPanelColumnNames.NAME,
+ selected: true,
+ configurable: true,
+ sortDirection: SortDirection.ASC,
+ filters: [],
+ render: uuid => <ResourceName uuid={uuid}/>,
+ width: "450px"
+ },
+ {
+ name: TrashPanelColumnNames.TYPE,
+ selected: true,
+ configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: [
+ {
+ name: resourceLabel(ResourceKind.COLLECTION),
+ selected: true,
+ type: ResourceKind.COLLECTION
+ },
+ {
+ name: resourceLabel(ResourceKind.PROCESS),
+ selected: true,
+ type: ResourceKind.PROCESS
+ },
+ {
+ name: resourceLabel(ResourceKind.PROJECT),
+ selected: true,
+ type: ResourceKind.PROJECT
+ }
+ ],
+ render: uuid => <ResourceType uuid={uuid}/>,
+ width: "125px"
+ },
+ {
+ name: TrashPanelColumnNames.FILE_SIZE,
+ selected: true,
+ configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: [],
+ render: uuid => <ResourceFileSize uuid={uuid} />,
+ width: "50px"
+ },
+ {
+ name: TrashPanelColumnNames.TRASHED_DATE,
+ selected: true,
+ configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: [],
+ render: uuid => <ResourceTrashDate uuid={uuid} />,
+ width: "50px"
+ },
+ {
+ name: TrashPanelColumnNames.TO_BE_DELETED,
+ selected: true,
+ configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: [],
+ render: uuid => <ResourceDeleteDate uuid={uuid} />,
+ width: "50px"
+ },
+ {
+ name: '',
+ selected: true,
+ configurable: false,
+ sortDirection: SortDirection.NONE,
+ filters: [],
+ render: uuid => <ResourceRestore uuid={uuid}/>,
+ width: "50px"
+ }
+];
+
+interface TrashPanelDataProps {
+ currentItemId: string;
+ resources: ResourcesState;
+}
+
+type TrashPanelProps = TrashPanelDataProps & DispatchProp & WithStyles<CssRules>;
+
+export const TrashPanel = withStyles(styles)(
+ connect((state: RootState) => ({
+ currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
+ resources: state.resources
+ }))(
+ class extends React.Component<TrashPanelProps> {
+ render() {
+ return <DataExplorer
+ id={TRASH_PANEL_ID}
+ onRowClick={this.handleRowClick}
+ onRowDoubleClick={this.handleRowDoubleClick}
+ onContextMenu={this.handleContextMenu}
+ defaultIcon={TrashIcon}
+ defaultMessages={['Your trash list is empty.']}
+ contextMenuColumn={false}/>
+ ;
+ }
+
+ handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+ const resource = getResource<TrashableResource>(resourceUuid)(this.props.resources);
+ if (resource) {
+ this.props.dispatch<any>(openContextMenu(event, {
+ name: '',
+ uuid: resource.uuid,
+ ownerUuid: resource.ownerUuid,
+ isTrashed: resource.isTrashed,
+ kind: resource.kind,
+ menuKind: ContextMenuKind.TRASH
+ }));
+ }
+ }
+
+ handleRowDoubleClick = (uuid: string) => {
+ this.props.dispatch<any>(navigateTo(uuid));
+ }
+
+ handleRowClick = (uuid: string) => {
+ this.props.dispatch(loadDetailsPanel(uuid));
+ }
+ }
+ )
+);
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>
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;
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);