//
// SPDX-License-Identifier: AGPL-3.0
-export const formatDate = (isoDate: string) => {
- const date = new Date(isoDate);
- const text = date.toLocaleString();
- return text === 'Invalid Date' ? "" : text;
+export const formatDate = (isoDate?: string) => {
+ if (isoDate) {
+ const date = new Date(isoDate);
+ const text = date.toLocaleString();
+ return text === 'Invalid Date' ? "" : text;
+ }
+ return "";
};
export const formatFileSize = (size?: number) => {
return <TableCell key={key || index} style={{ width: column.width, minWidth: column.width }}>
{renderHeader ?
renderHeader() :
- filters
+ filters.length > 0
? <DataTableFilters
name={`${name} filters`}
onChange={filters =>
//
// 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";
-
-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;
- 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 { 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_REQUEST = "arvados#containerRequest",
GROUP = "arvados#group",
PROCESS = "arvados#containerRequest",
PROJECT = "arvados#group",
//
// 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";
order?: string;
filters?: string;
recursive?: boolean;
+ includeTrash?: boolean;
}
export type GroupContentsResource =
import { TagService } from "./tag-service/tag-service";
import { CollectionFilesService } from "./collection-files-service/collection-files-service";
import { KeepService } from "./keep-service/keep-service";
-import { WebDAV } from "../common/webdav";
-import { Config } from "../common/config";
+import { WebDAV } from "~/common/webdav";
+import { Config } from "~/common/config";
+import { TrashService } from "~/services/trash-service/trash-service";
export type ServiceRepository = ReturnType<typeof createServices>;
const projectService = new ProjectService(apiClient);
const linkService = new LinkService(apiClient);
const favoriteService = new FavoriteService(linkService, groupsService);
+ const trashService = new TrashService(apiClient);
const collectionService = new CollectionService(apiClient, keepService, webdavClient, authService);
const tagService = new TagService(linkService);
const collectionFilesService = new CollectionFilesService(collectionService);
projectService,
linkService,
favoriteService,
+ trashService,
collectionService,
tagService,
collectionFilesService
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { GroupsService } from "../groups-service/groups-service";
+import { TrashService } from "./trash-service";
+import { mockResourceService } from "~/common/api/common-resource-service.test";
+
+describe("TrashService", () => {
+
+ let groupService: GroupsService;
+
+ beforeEach(() => {
+ groupService = mockResourceService(GroupsService);
+ });
+
+});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { GroupsService } from "../groups-service/groups-service";
+import { AxiosInstance } from "axios";
+
+export class TrashService extends GroupsService {
+ constructor(serverApi: AxiosInstance) {
+ super(serverApi);
+ }
+}
import { favoritePanelActions } from "../favorite-panel/favorite-panel-action";
import { projectPanelActions } from "../project-panel/project-panel-action";
import { projectActions } from "../project/project-action";
-import { getProjectUrl } from "../../models/project";
-import { columns as projectPanelColumns } from "../../views/project-panel/project-panel";
-import { columns as favoritePanelColumns } from "../../views/favorite-panel/favorite-panel";
+import { getProjectUrl } from "~/models/project";
+import { columns as projectPanelColumns } from "~/views/project-panel/project-panel";
+import { columns as favoritePanelColumns } from "~/views/favorite-panel/favorite-panel";
+import { columns as trashPanelColumns } from "~/views/trash-panel/trash-panel";
+import { trashPanelActions } from "~/store/trash-panel/trash-panel-action";
export type SidePanelState = SidePanelItem[];
active: false,
activeAction: (dispatch: Dispatch) => {
dispatch(push("/trash"));
+ dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
+ dispatch(trashPanelActions.RESET_PAGINATION());
+ dispatch(trashPanelActions.REQUEST_ITEMS());
}
}
];
import { ServiceRepository } from "~/services/services";
import { treePickerReducer } from './tree-picker/tree-picker-reducer';
import { TreePicker } from './tree-picker/tree-picker';
+import { TrashPanelMiddlewareService } from "~/store/trash-panel/trash-panel-middleware-service";
+import { TRASH_PANEL_ID } from "~/store/trash-panel/trash-panel-action";
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
const favoritePanelMiddleware = dataExplorerMiddleware(
new FavoritePanelMiddlewareService(services, FAVORITE_PANEL_ID)
);
+ const trashPanelMiddleware = dataExplorerMiddleware(
+ new TrashPanelMiddlewareService(services, TRASH_PANEL_ID)
+ );
const middlewares: Middleware[] = [
routerMiddleware(history),
thunkMiddleware.withExtraArgument(services),
projectPanelMiddleware,
- favoritePanelMiddleware
+ favoritePanelMiddleware,
+ trashPanelMiddleware
];
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
return createStore(rootReducer, enhancer);
--- /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.PROCESS)
+ .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROJECT);
+ }
+
+ const userUuid = this.services.authService.getUuid()!;
+
+ this.services.trashService
+ .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.PROCESS)
+ .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
+ }));
+ });
+ }
+}
}
};
-export const renderDate = (date: string) => {
+export const renderDate = (date?: string) => {
return <Typography noWrap>{formatDate(date)}</Typography>;
};
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 FavoritePanelFilter extends DataTableFilterItem {
- type: ResourceKind | ContainerRequestState;
+ type: ResourceKind | ProcessState;
}
export const columns: DataColumns<FavoritePanelItem, 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: renderStatus,
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 columns: DataColumns<ProjectPanelItem, 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: renderStatus,
--- /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;
+ fileSize?: number;
+ trashAt?: string;
+ deleteAt?: string;
+ isTrashed?: boolean;
+}
+
+export function resourceToDataItem(r: GroupContentsResource): TrashPanelItem {
+ return {
+ uuid: r.uuid,
+ name: r.name,
+ kind: r.kind,
+ 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 { ProcessState } from '~/models/process';
+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 | ProcessState;
+}
+
+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 { DialogCollectionCreateWithSelectedFile } from '~/views-components/create-collection-dialog-with-selected/create-collection-dialog-with-selected';
import { COLLECTION_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-collection-create';
import { PROJECT_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-project-create';
+import { TrashPanel } from "~/views/trash-panel/trash-panel";
+import { trashPanelActions } from "~/store/trash-panel/trash-panel-action";
const DRAWER_WITDH = 240;
const APP_BAR_HEIGHT = 100;
<Route path='/' exact render={() => <Redirect to={`/projects/${this.props.authService.getUuid()}`} />} />
<Route path="/projects/:id" render={this.renderProjectPanel} />
<Route path="/favorites" render={this.renderFavoritePanel} />
+ <Route path="/trash" render={this.renderTrashPanel} />
<Route path="/collections/:id" render={this.renderCollectionPanel} />
</Switch>
</div>
}}
{...props} />
+ renderTrashPanel = (props: RouteComponentProps<{ id: string }>) => <TrashPanel
+ onItemRouteChange={() => this.props.dispatch(trashPanelActions.REQUEST_ITEMS())}
+ onContextMenu={(event, item) => {
+ const kind = item.kind === ResourceKind.PROJECT ? ContextMenuKind.PROJECT : ContextMenuKind.RESOURCE;
+ this.openContextMenu(event, {
+ uuid: item.uuid,
+ name: item.name,
+ kind,
+ });
+ }}
+ onDialogOpen={this.handleProjectCreationDialogOpen}
+ onItemClick={item => {
+ this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
+ }}
+ onItemDoubleClick={item => {
+ switch (item.kind) {
+ case ResourceKind.COLLECTION:
+ this.props.dispatch(loadCollection(item.uuid));
+ this.props.dispatch(push(getCollectionUrl(item.uuid)));
+ default:
+ this.props.dispatch(loadDetails(item.uuid, ResourceKind.PROJECT));
+ this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
+ }
+
+ }}
+ {...props} />
+
mainAppBarActions: MainAppBarActionProps = {
onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
this.props.dispatch(setProjectItem(itemId, ItemMode.BOTH));