//
// 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) => {
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/RestoreFromTrash';
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} />;
//
// SPDX-License-Identifier: AGPL-3.0
-import { Resource, ResourceKind } from "./resource";
+import { ResourceKind, TrashResource } from "./resource";
-export interface CollectionResource extends Resource {
+export interface CollectionResource extends TrashResource {
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
+};
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { Resource, ResourceKind } from "./resource";
-import { MountType } from "~/models/mount-types";
-import { RuntimeConstraints } from './runtime-constraints';
-import { SchedulingParameters } from './scheduling-parameters';
-
-export enum ContainerRequestState {
- UNCOMMITTED = "Uncommitted",
- COMMITTED = "Committed",
- FINAL = "Final"
-}
-
-export interface ContainerRequestResource extends Resource {
- kind: ResourceKind.CONTAINER_REQUEST;
- name: string;
- description: string;
- properties: any;
- state: ContainerRequestState;
- requestingContainerUuid: string | null;
- containerUuid: string | null;
- containerCountMax: number;
- mounts: MountType[];
- runtimeConstraints: RuntimeConstraints;
- schedulingParameters: SchedulingParameters;
- containerImage: string;
- environment: any;
- cwd: string;
- command: string[];
- outputPath: string;
- outputName: string;
- outputTtl: number;
- priority: number | null;
- expiresAt: string;
- useExisting: boolean;
- logUuid: string | null;
- outputUuid: string | null;
- filters: string;
-}
//
// SPDX-License-Identifier: AGPL-3.0
-import { Resource, ResourceKind } from "./resource";
+import { ResourceKind, TrashResource } from "./resource";
-export interface GroupResource extends Resource {
+export interface GroupResource extends TrashResource {
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 { ContainerRequestResource } from "./container-request";
+import { Resource, ResourceKind } from "./resource";
-export type ProcessResource = ContainerRequestResource;
+export enum ProcessState {
+ UNCOMMITTED = "Uncommitted",
+ COMMITTED = "Committed",
+ FINAL = "Final"
+}
+
+export interface ProcessResource extends Resource {
+ kind: ResourceKind.PROCESS;
+ name: string;
+ description: string;
+ properties: any;
+ state: ProcessState;
+ requestingContainerUuid: string;
+ containerUuid: string;
+ containerCountMax: number;
+ mounts: any;
+ runtimeConstraints: any;
+ schedulingParameters: any;
+ containerImage: string;
+ environment: any;
+ cwd: string;
+ command: string[];
+ outputPath: string;
+ outputName: string;
+ outputTtl: number;
+ priority: number;
+ expiresAt: string;
+ useExisting: boolean;
+ logUuid: string;
+ outputUuid: string;
+ filters: string;
+}
//
// SPDX-License-Identifier: AGPL-3.0
-import { GroupResource, GroupClass } from "./group";
+import { GroupClass, GroupResource } from "./group";
export interface ProjectResource extends GroupResource {
groupClass: GroupClass.PROJECT;
etag: string;
}
+export interface TrashResource extends Resource {
+ trashAt: string;
+ deleteAt: string;
+ isTrashed: boolean;
+}
+
export enum ResourceKind {
COLLECTION = "arvados#collection",
CONTAINER = "arvados#container",
//
// 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";
}
+ trash(uuid: string): Promise<CollectionResource> {
+ return this.serverApi
+ .post(this.resourceType + `${uuid}/trash`)
+ .then(CommonResourceService.mapResponseKeys);
+ }
+
+ untrash(uuid: string): Promise<CollectionResource> {
+ const params = {
+ ensure_unique_name: true
+ };
+ return this.serverApi
+ .post(this.resourceType + `${uuid}/untrash`, {
+ params: CommonResourceService.mapKeys(_.snakeCase)(params)
+ })
+ .then(CommonResourceService.mapResponseKeys);
+ }
+
}
import * as _ from "lodash";
import { CommonResourceService, ListResults } from "~/common/api/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 { TrashResource } from "~/models/resource";
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 TrashResource = TrashResource> extends CommonResourceService<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);
}
+
+ 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);
+ }
+
}
export enum GroupContentsResourcePrefix {
--- /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 { sidePanelActions } from "~/store/side-panel/side-panel-action";
+import { SidePanelId } from "~/store/side-panel/side-panel-reducer";
+import { trashPanelActions } from "~/store/trash-panel/trash-panel-action";
+import { getProjectList, projectActions } from "~/store/project/project-action";
+
+export const toggleCollectionTrashed = (resource: { uuid: string; name: string, isTrashed?: boolean, ownerUuid?: string }) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Working..." }));
+ if (resource.isTrashed) {
+ return services.collectionService.untrash(resource.uuid).then(() => {
+ dispatch<any>(getProjectList(resource.ownerUuid)).then(() => {
+ dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(SidePanelId.PROJECTS));
+ dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN({ itemId: resource.ownerUuid!!, open: true, recursive: true }));
+ });
+ dispatch(trashPanelActions.REQUEST_ITEMS());
+ dispatch(snackbarActions.CLOSE_SNACKBAR());
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Restored from trash",
+ hideDuration: 2000
+ }));
+ });
+ } else {
+ return services.collectionService.trash(resource.uuid).then(() => {
+ dispatch(snackbarActions.CLOSE_SNACKBAR());
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Added to trash",
+ hideDuration: 2000
+ }));
+ });
+ }
+ };
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';
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: 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: 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: ContextMenuKind.PROJECT,
+ ownerUuid: res.ownerUuid,
+ isTrashed: res.isTrashed
}));
}
};
kind: string;
name: string;
description?: string;
+ isTrashed?: boolean;
+ ownerUuid?: string;
}
const initialState = {
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+import { default as unionize, ofType, UnionOf } from "unionize";
+
+import { ProjectResource } from "~/models/project";
+import { Dispatch } from "redux";
+import { FilterBuilder } from "~/common/api/filter-builder";
+import { RootState } from "../store";
+import { checkPresenceInFavorites } from "../favorites/favorites-actions";
+import { ServiceRepository } from "~/services/services";
+import { projectPanelActions } from "~/store/project-panel/project-panel-action";
+import { updateDetails } from "~/store/details-panel/details-panel-action";
+import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { trashPanelActions } from "~/store/trash-panel/trash-panel-action";
+import { sidePanelActions } from "~/store/side-panel/side-panel-action";
+import { SidePanelId } from "~/store/side-panel/side-panel-reducer";
+
+export const projectActions = unionize({
+ OPEN_PROJECT_CREATOR: ofType<{ ownerUuid: string }>(),
+ CLOSE_PROJECT_CREATOR: ofType<{}>(),
+ CREATE_PROJECT: ofType<Partial<ProjectResource>>(),
+ CREATE_PROJECT_SUCCESS: ofType<ProjectResource>(),
+ OPEN_PROJECT_UPDATER: ofType<{ uuid: string}>(),
+ CLOSE_PROJECT_UPDATER: ofType<{}>(),
+ UPDATE_PROJECT_SUCCESS: ofType<ProjectResource>(),
+ REMOVE_PROJECT: ofType<string>(),
+ PROJECTS_REQUEST: ofType<string>(),
+ PROJECTS_SUCCESS: ofType<{ projects: ProjectResource[], parentItemId?: string }>(),
+ TOGGLE_PROJECT_TREE_ITEM_OPEN: ofType<{ itemId: string, open?: boolean, recursive?: boolean }>(),
+ TOGGLE_PROJECT_TREE_ITEM_ACTIVE: ofType<{ itemId: string, active?: boolean, recursive?: boolean }>(),
+ RESET_PROJECT_TREE_ACTIVITY: ofType<string>()
+}, {
+ tag: 'type',
+ value: 'payload'
+});
+
+export const PROJECT_FORM_NAME = 'projectEditDialog';
+
+export const getProjectList = (parentUuid: string = '') =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(projectActions.PROJECTS_REQUEST(parentUuid));
+ return services.projectService.list({
+ filters: new FilterBuilder()
+ .addEqual("ownerUuid", parentUuid)
+ .getFilters()
+ }).then(({ items: projects }) => {
+ dispatch(projectActions.PROJECTS_SUCCESS({ projects, parentItemId: parentUuid }));
+ dispatch<any>(checkPresenceInFavorites(projects.map(project => project.uuid)));
+ return projects;
+ });
+ };
+
+export const createProject = (project: Partial<ProjectResource>) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { ownerUuid } = getState().projects.creator;
+ const projectData = { ownerUuid, ...project };
+ dispatch(projectActions.CREATE_PROJECT(projectData));
+ return services.projectService
+ .create(projectData)
+ .then(project => dispatch(projectActions.CREATE_PROJECT_SUCCESS(project)));
+ };
+
+export const updateProject = (project: Partial<ProjectResource>) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { uuid } = getState().projects.updater;
+ return services.projectService
+ .update(uuid, project)
+ .then(project => {
+ dispatch(projectActions.UPDATE_PROJECT_SUCCESS(project));
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+ dispatch<any>(getProjectList(project.ownerUuid));
+ dispatch<any>(updateDetails(project));
+ });
+ };
+
+export const toggleProjectTrashed = (resource: { uuid: string; name: string, isTrashed?: boolean, ownerUuid?: string }) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Working..." }));
+ if (resource.isTrashed) {
+ return services.groupsService.untrash(resource.uuid).then(() => {
+ dispatch<any>(getProjectList(resource.ownerUuid)).then(() => {
+ dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(SidePanelId.PROJECTS));
+ dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN({ itemId: resource.ownerUuid!!, open: true, recursive: true }));
+ });
+ dispatch(trashPanelActions.REQUEST_ITEMS());
+ dispatch(snackbarActions.CLOSE_SNACKBAR());
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Restored from trash",
+ hideDuration: 2000
+ }));
+ });
+ } else {
+ return services.groupsService.trash(resource.uuid).then(() => {
+ dispatch<any>(getProjectList(resource.ownerUuid)).then(() => {
+ dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN({ itemId: resource.ownerUuid!!, open: true, recursive: true }));
+ });
+ dispatch(snackbarActions.CLOSE_SNACKBAR());
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Added to trash",
+ hideDuration: 2000
+ }));
+ });
+ }
+ };
+
+export type ProjectAction = UnionOf<typeof projectActions>;
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";
+
+export const TRASH_PANEL_ID = "trashPanel";
+export const trashPanelActions = bindDataExplorerActions(TRASH_PANEL_ID);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { DataExplorerMiddlewareService } 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 "~/common/api/filter-builder";
+import { checkPresenceInFavorites } from "../favorites/favorites-actions";
+import { trashPanelActions } from "./trash-panel-action";
+import { Dispatch, MiddlewareAPI } from "redux";
+import { OrderBuilder, OrderDirection } from "~/common/api/order-builder";
+import { GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
+import { resourceToDataItem, TrashPanelItem } from "~/views/trash-panel/trash-panel-item";
+import { TrashPanelColumnNames, TrashPanelFilter } from "~/views/trash-panel/trash-panel";
+import { ProjectResource } from "~/models/project";
+import { ProjectPanelColumnNames } from "~/views/project-panel/project-panel";
+
+export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
+ constructor(private services: ServiceRepository, id: string) {
+ super(id);
+ }
+
+ requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+ const dataExplorer = api.getState().dataExplorer[this.getId()];
+ const columns = dataExplorer.columns as DataColumns<TrashPanelItem, 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);
+ }
+
+ const userUuid = this.services.authService.getUuid()!;
+
+ this.services.groupsService
+ .contents(userUuid, {
+ limit: dataExplorer.rowsPerPage,
+ offset: dataExplorer.page * dataExplorer.rowsPerPage,
+ 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
+ })
+ .then(response => {
+ api.dispatch(trashPanelActions.SET_ITEMS({
+ items: response.items.map(resourceToDataItem).filter(it => it.isTrashed),
+ itemsAvailable: response.itemsAvailable,
+ page: Math.floor(response.offset / response.limit),
+ rowsPerPage: response.limit
+ }));
+ api.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
+ })
+ .catch(() => {
+ api.dispatch(trashPanelActions.SET_ITEMS({
+ items: [],
+ itemsAvailable: 0,
+ page: 0,
+ rowsPerPage: dataExplorer.rowsPerPage
+ }));
+ });
+ }
+}
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/collections/collection-trash-actions";
export const collectionActionSet: ContextMenuActionSet = [[
{
});
}
},
+ {
+ component: ToggleTrashAction,
+ execute: (dispatch, resource) => {
+ dispatch<any>(toggleCollectionTrashed(resource));
+ }
+ },
{
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";
});
}
},
+ {
+ component: ToggleTrashAction,
+ execute: (dispatch, resource) => {
+ dispatch<any>(toggleCollectionTrashed(resource));
+ }
+ },
{
icon: CopyIcon,
name: "Copy to project",
//
// SPDX-License-Identifier: AGPL-3.0
+import { reset, initialize } from "redux-form";
+
import { ContextMenuActionSet } from "../context-menu-action-set";
-import { NewProjectIcon, RenameIcon, CopyIcon, MoveToIcon } from "~/components/icon/icon";
+import { projectActions, PROJECT_FORM_NAME, toggleProjectTrashed } from "~/store/project/project-action";
+import { NewProjectIcon, RenameIcon } from "~/components/icon/icon";
import { ToggleFavoriteAction } from "../actions/favorite-action";
import { toggleFavorite } from "~/store/favorites/favorites-actions";
import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
-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 { PROJECT_CREATE_DIALOG } from "../../dialog-create/dialog-project-create";
+import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
export const projectActionSet: ContextMenuActionSet = [[
{
icon: NewProjectIcon,
name: "New project",
execute: (dispatch, resource) => {
- dispatch<any>(openProjectCreateDialog(resource.uuid));
+ dispatch(reset(PROJECT_CREATE_DIALOG));
+ dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid }));
}
},
{
icon: RenameIcon,
name: "Edit project",
execute: (dispatch, resource) => {
- dispatch<any>(openProjectUpdateDialog(resource));
+ dispatch(projectActions.OPEN_PROJECT_UPDATER({ uuid: resource.uuid }));
+ dispatch(initialize(PROJECT_FORM_NAME, { name: resource.name, description: resource.description }));
}
},
{
}
},
{
- icon: MoveToIcon,
- name: "Move to",
- execute: (dispatch, resource) => dispatch<any>(openMoveProjectDialog(resource))
- },
- {
- icon: CopyIcon,
- name: "Copy to project",
+ component: ToggleTrashAction,
execute: (dispatch, resource) => {
- // add code
+ dispatch<any>(toggleProjectTrashed(resource));
}
- },
+ }
]];
--- /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 >);
}
};
-export const renderDate = (date: string) => {
+export const renderDate = (date?: string) => {
return <Typography noWrap>{formatDate(date)}</Typography>;
};
};
})(
class extends React.Component<CollectionPanelProps> {
-
render() {
const { classes, item, tags } = this.props;
return <div>
hideDuration: 2000
}));
}
-
}
)
);
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 { ProcessState } from '~/models/process';
import { SortDirection } from '~/components/data-table/data-column';
import { ResourceKind } from '~/models/resource';
import { resourceLabel } from '~/common/labels';
}
export interface FavoritePanelFilter extends DataTableFilterItem {
- type: ResourceKind | ContainerRequestState;
+ type: ResourceKind | ProcessState;
}
export const favoritePanelColumns: DataColumns<string, FavoritePanelFilter> = [
sortDirection: SortDirection.NONE,
filters: [
{
- name: ContainerRequestState.COMMITTED,
+ name: ProcessState.COMMITTED,
selected: true,
- type: ContainerRequestState.COMMITTED
+ type: ProcessState.COMMITTED
},
{
- name: ContainerRequestState.FINAL,
+ name: ProcessState.FINAL,
selected: true,
- type: ContainerRequestState.FINAL
+ type: ProcessState.FINAL
},
{
- name: ContainerRequestState.UNCOMMITTED,
+ name: ProcessState.UNCOMMITTED,
selected: true,
- type: ContainerRequestState.UNCOMMITTED
+ type: ProcessState.UNCOMMITTED
}
],
render: uuid => <ProcessStatus uuid={uuid} />,
import { RouteComponentProps } from 'react-router';
import { RootState } from '~/store/store';
import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
-import { ContainerRequestState } from '~/models/container-request';
+import { ProcessState } from '~/models/process';
import { SortDirection } from '~/components/data-table/data-column';
import { ResourceKind } from '~/models/resource';
import { resourceLabel } from '~/common/labels';
}
export interface ProjectPanelFilter extends DataTableFilterItem {
- type: ResourceKind | ContainerRequestState;
+ type: ResourceKind | ProcessState;
}
export const projectPanelColumns: DataColumns<string, ProjectPanelFilter> = [
sortDirection: SortDirection.NONE,
filters: [
{
- name: ContainerRequestState.COMMITTED,
+ name: ProcessState.COMMITTED,
selected: true,
- type: ContainerRequestState.COMMITTED
+ type: ProcessState.COMMITTED
},
{
- name: ContainerRequestState.FINAL,
+ name: ProcessState.FINAL,
selected: true,
- type: ContainerRequestState.FINAL
+ type: ProcessState.FINAL
},
{
- name: ContainerRequestState.UNCOMMITTED,
+ name: ProcessState.UNCOMMITTED,
selected: true,
- type: ContainerRequestState.UNCOMMITTED
+ type: ProcessState.UNCOMMITTED
}
],
render: uuid => <ProcessStatus uuid={uuid} />,
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { GroupContentsResource } from "~/services/groups-service/groups-service";
+import { TrashResource } from "~/models/resource";
+
+export interface TrashPanelItem {
+ uuid: string;
+ name: string;
+ kind: string;
+ owner: string;
+ fileSize?: number;
+ trashAt?: string;
+ deleteAt?: string;
+ isTrashed?: boolean;
+}
+
+export function resourceToDataItem(r: GroupContentsResource): TrashPanelItem {
+ return {
+ uuid: r.uuid,
+ name: r.name,
+ kind: r.kind,
+ owner: r.ownerUuid,
+ trashAt: (r as TrashResource).trashAt,
+ deleteAt: (r as TrashResource).deleteAt,
+ isTrashed: (r as TrashResource).isTrashed
+ };
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { TrashPanelItem } from './trash-panel-item';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
+import { DispatchProp, connect } from 'react-redux';
+import { DataColumns } from '~/components/data-table/data-table';
+import { RouteComponentProps } from 'react-router';
+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 } from '~/models/resource';
+import { resourceLabel } from '~/common/labels';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { renderName, renderType, renderFileSize, renderDate } from '~/views-components/data-explorer/renderers';
+import { TrashIcon } from '~/components/icon/icon';
+import { TRASH_PANEL_ID } from "~/store/trash-panel/trash-panel-action";
+
+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 columns: DataColumns<TrashPanelItem, TrashPanelFilter> = [
+ {
+ name: TrashPanelColumnNames.NAME,
+ selected: true,
+ configurable: true,
+ sortDirection: SortDirection.ASC,
+ filters: [],
+ render: renderName,
+ 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: item => renderType(item.kind),
+ width: "125px"
+ },
+ {
+ name: TrashPanelColumnNames.FILE_SIZE,
+ selected: true,
+ configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: [],
+ render: item => renderFileSize(item.fileSize),
+ width: "50px"
+ },
+ {
+ name: TrashPanelColumnNames.TRASHED_DATE,
+ selected: true,
+ configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: [],
+ render: item => renderDate(item.trashAt),
+ width: "50px"
+ },
+ {
+ name: TrashPanelColumnNames.TO_BE_DELETED,
+ selected: true,
+ configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: [],
+ render: item => renderDate(item.deleteAt),
+ width: "50px"
+ },
+];
+
+interface TrashPanelDataProps {
+ currentItemId: string;
+}
+
+interface TrashPanelActionProps {
+ onItemClick: (item: TrashPanelItem) => void;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TrashPanelItem) => void;
+ onDialogOpen: (ownerUuid: string) => void;
+ onItemDoubleClick: (item: TrashPanelItem) => void;
+ onItemRouteChange: (itemId: string) => void;
+}
+
+type TrashPanelProps = TrashPanelDataProps & TrashPanelActionProps & DispatchProp
+ & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+
+export const TrashPanel = withStyles(styles)(
+ connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))(
+ class extends React.Component<TrashPanelProps> {
+ render() {
+ return <DataExplorer
+ id={TRASH_PANEL_ID}
+ columns={columns}
+ onRowClick={this.props.onItemClick}
+ onRowDoubleClick={this.props.onItemDoubleClick}
+ onContextMenu={this.props.onContextMenu}
+ extractKey={(item: TrashPanelItem) => item.uuid}
+ defaultIcon={TrashIcon}
+ defaultMessages={['Your trash list is empty.']}/>
+ ;
+ }
+
+ componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: TrashPanelProps) {
+ if (match.params.id !== currentItemId) {
+ onItemRouteChange(match.params.id);
+ }
+ }
+ }
+ )
+);
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";
+import { trashPanelActions } from "~/store/trash-panel/trash-panel-action";
+
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="/trash" render={this.renderTrashPanel} />
</Switch>
</div>
{user && <DetailsPanel />}