1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import React from 'react';
6 import withStyles from '@material-ui/core/styles/withStyles';
7 import { DispatchProp, connect } from 'react-redux';
8 import { RouteComponentProps } from 'react-router';
9 import { StyleRulesCallback, WithStyles } from '@material-ui/core';
11 import { DataExplorer } from 'views-components/data-explorer/data-explorer';
12 import { DataColumns } from 'components/data-table/data-table';
13 import { RootState } from 'store/store';
14 import { DataTableFilterItem } from 'components/data-table-filters/data-table-filters';
15 import { ContainerRequestState } from 'models/container-request';
16 import { SortDirection } from 'components/data-table/data-column';
17 import { ResourceKind, Resource } from 'models/resource';
20 ProcessStatus as ResourceStatus,
22 ResourceOwnerWithName,
23 ResourcePortableDataHash,
27 ResourceContainerUuid,
31 ResourceParentProcess,
32 ResourceModifiedByUserUuid,
34 ResourceCreatedAtDate,
35 ResourceLastModifiedDate,
38 } from 'views-components/data-explorer/renderers';
39 import { ProjectIcon } from 'components/icon/icon';
40 import { ResourcesState, getResource } from 'store/resources/resources';
41 import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
42 import { openContextMenu, resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions';
43 import { navigateTo } from 'store/navigation/navigation-action';
44 import { getProperty } from 'store/properties/properties';
45 import { PROJECT_PANEL_CURRENT_UUID } from 'store/project-panel/project-panel-action';
46 import { ArvadosTheme } from 'common/custom-theme';
47 import { createTree } from 'models/tree';
48 import { getInitialResourceTypeFilters, getInitialProcessStatusFilters } from 'store/resource-type-filters/resource-type-filters';
49 import { GroupContentsResource } from 'services/groups-service/groups-service';
50 import { GroupClass, GroupResource } from 'models/group';
51 import { CollectionResource } from 'models/collection';
52 import { resourceIsFrozen } from 'common/frozen-resources';
53 import { ProjectResource } from 'models/project';
54 import { NotFoundView } from 'views/not-found-panel/not-found-panel';
55 import { deselectAllOthers, toggleOne } from 'store/multiselect/multiselect-actions';
56 import { PendingIcon } from 'components/icon/icon';
57 import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
59 type CssRules = 'root' | 'button' | 'loader' | 'notFoundView';
61 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
66 marginLeft: theme.spacing.unit,
82 export enum ProjectPanelColumnNames {
87 PORTABLE_DATA_HASH = 'Portable Data Hash',
88 FILE_SIZE = 'File Size',
89 FILE_COUNT = 'File Count',
91 CONTAINER_UUID = 'Container UUID',
93 OUTPUT_UUID = 'Output UUID',
94 LOG_UUID = 'Log UUID',
95 PARENT_PROCESS = 'Parent Process UUID',
96 MODIFIED_BY_USER_UUID = 'Modified by User UUID',
98 CREATED_AT = 'Date Created',
99 LAST_MODIFIED = 'Last Modified',
100 TRASH_AT = 'Trash at',
101 DELETE_AT = 'Delete at',
104 export interface ProjectPanelFilter extends DataTableFilterItem {
105 type: ResourceKind | ContainerRequestState;
108 export const projectPanelColumns: DataColumns<string, ProjectResource> = [
110 name: ProjectPanelColumnNames.NAME,
113 sort: { direction: SortDirection.NONE, field: 'name' },
114 filters: createTree(),
115 render: (uuid) => <ResourceName uuid={uuid} />,
118 name: ProjectPanelColumnNames.STATUS,
121 mutuallyExclusiveFilters: true,
122 filters: getInitialProcessStatusFilters(),
123 render: (uuid) => <ResourceStatus uuid={uuid} />,
126 name: ProjectPanelColumnNames.TYPE,
129 filters: getInitialResourceTypeFilters(),
130 render: (uuid) => <ResourceType uuid={uuid} />,
133 name: ProjectPanelColumnNames.OWNER,
136 filters: createTree(),
137 render: (uuid) => <ResourceOwnerWithName uuid={uuid} />,
140 name: ProjectPanelColumnNames.PORTABLE_DATA_HASH,
143 filters: createTree(),
144 render: (uuid) => <ResourcePortableDataHash uuid={uuid} />,
147 name: ProjectPanelColumnNames.FILE_SIZE,
150 filters: createTree(),
151 render: (uuid) => <ResourceFileSize uuid={uuid} />,
154 name: ProjectPanelColumnNames.FILE_COUNT,
157 filters: createTree(),
158 render: (uuid) => <ResourceFileCount uuid={uuid} />,
161 name: ProjectPanelColumnNames.UUID,
164 filters: createTree(),
165 render: (uuid) => <ResourceUUID uuid={uuid} />,
168 name: ProjectPanelColumnNames.CONTAINER_UUID,
171 filters: createTree(),
172 render: (uuid) => <ResourceContainerUuid uuid={uuid} />,
175 name: ProjectPanelColumnNames.RUNTIME,
178 filters: createTree(),
179 render: (uuid) => <ContainerRunTime uuid={uuid} />,
182 name: ProjectPanelColumnNames.OUTPUT_UUID,
185 filters: createTree(),
186 render: (uuid) => <ResourceOutputUuid uuid={uuid} />,
189 name: ProjectPanelColumnNames.LOG_UUID,
192 filters: createTree(),
193 render: (uuid) => <ResourceLogUuid uuid={uuid} />,
196 name: ProjectPanelColumnNames.PARENT_PROCESS,
199 filters: createTree(),
200 render: (uuid) => <ResourceParentProcess uuid={uuid} />,
203 name: ProjectPanelColumnNames.MODIFIED_BY_USER_UUID,
206 filters: createTree(),
207 render: (uuid) => <ResourceModifiedByUserUuid uuid={uuid} />,
210 name: ProjectPanelColumnNames.VERSION,
213 filters: createTree(),
214 render: (uuid) => <ResourceVersion uuid={uuid} />,
217 name: ProjectPanelColumnNames.CREATED_AT,
220 sort: { direction: SortDirection.NONE, field: 'createdAt' },
221 filters: createTree(),
222 render: (uuid) => <ResourceCreatedAtDate uuid={uuid} />,
225 name: ProjectPanelColumnNames.LAST_MODIFIED,
228 sort: { direction: SortDirection.DESC, field: 'modifiedAt' },
229 filters: createTree(),
230 render: (uuid) => <ResourceLastModifiedDate uuid={uuid} />,
233 name: ProjectPanelColumnNames.TRASH_AT,
236 sort: { direction: SortDirection.NONE, field: 'trashAt' },
237 filters: createTree(),
238 render: (uuid) => <ResourceTrashDate uuid={uuid} />,
241 name: ProjectPanelColumnNames.DELETE_AT,
244 sort: { direction: SortDirection.NONE, field: 'deleteAt' },
245 filters: createTree(),
246 render: (uuid) => <ResourceDeleteDate uuid={uuid} />,
250 export const PROJECT_PANEL_ID = 'projectPanel';
252 const DEFAULT_VIEW_MESSAGES = ['Your project is empty.', 'Please create a project or create a collection and upload a data.'];
254 interface ProjectPanelDataProps {
255 currentItemId: string;
256 resources: ResourcesState;
257 project: GroupResource;
260 dataExplorerItems: any;
264 type ProjectPanelProps = ProjectPanelDataProps & DispatchProp & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
266 const mapStateToProps = (state: RootState) => {
267 const currentItemId = getProperty<string>(PROJECT_PANEL_CURRENT_UUID)(state.properties);
268 const project = getResource<GroupResource>(currentItemId || "")(state.resources);
269 const working = !!state.progressIndicator.some(p => p.id === PROJECT_PANEL_ID && p.working);
274 resources: state.resources,
275 userUuid: state.auth.user!.uuid,
279 type ProjectPanelState = {
283 export const ProjectPanel = withStyles(styles)(
284 connect(mapStateToProps)(
285 class extends React.Component<ProjectPanelProps> {
287 state: ProjectPanelState ={
291 componentDidMount(): void {
292 this.setState({ isLoaded: false });
295 componentDidUpdate( prevProps: Readonly<ProjectPanelProps>, prevState: Readonly<{}>, snapshot?: any ): void {
296 if(prevProps.working === true && this.props.working === false) {
297 this.setState({ isLoaded: true });
302 const { classes } = this.props;
304 return this.props.project ?
305 <div data-cy='project-panel' className={classes.root}>
307 id={PROJECT_PANEL_ID}
308 onRowClick={this.handleRowClick}
309 onRowDoubleClick={this.handleRowDoubleClick}
310 onContextMenu={this.handleContextMenu}
311 contextMenuColumn={true}
312 defaultViewIcon={ProjectIcon}
313 defaultViewMessages={DEFAULT_VIEW_MESSAGES}
316 : this.state.isLoaded ?
317 <div className={classes.notFoundView}>
320 messages={["Project not found"]}
324 <div className={classes.loader}>
325 <DataTableDefaultView
327 messages={["Loading data, please wait."]}
332 isCurrentItemChild = (resource: Resource) => {
333 return resource.ownerUuid === this.props.currentItemId;
336 handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
337 const { resources, isAdmin } = this.props;
338 const resource = getResource<GroupContentsResource>(resourceUuid)(resources);
339 // When viewing the contents of a filter group, all contents should be treated as read only.
340 let readonly = false;
341 const project = getResource<GroupResource>(this.props.currentItemId)(resources);
342 if (project && project.groupClass === GroupClass.FILTER) {
346 const menuKind = this.props.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid, readonly));
347 if (menuKind && resource) {
348 this.props.dispatch<any>(
349 openContextMenu(event, {
352 ownerUuid: resource.ownerUuid,
353 isTrashed: 'isTrashed' in resource ? resource.isTrashed : false,
357 isFrozen: resourceIsFrozen(resource, resources),
358 description: resource.description,
359 storageClassesDesired: (resource as CollectionResource).storageClassesDesired,
360 properties: 'properties' in resource ? resource.properties : {},
364 this.props.dispatch<any>(loadDetailsPanel(resourceUuid));
367 handleRowDoubleClick = (uuid: string) => {
368 this.props.dispatch<any>(navigateTo(uuid));
371 handleRowClick = (uuid: string) => {
372 this.props.dispatch<any>(toggleOne(uuid))
373 this.props.dispatch<any>(deselectAllOthers(uuid))
374 this.props.dispatch<any>(loadDetailsPanel(uuid));