17205: Fixed types
[arvados-workbench2.git] / src / views-components / data-explorer / renderers.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import * as React from 'react';
6 import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox } from '@material-ui/core';
7 import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star';
8 import { ResourceKind, TrashableResource } from '~/models/resource';
9 import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon, ShareIcon, CollectionOldVersionIcon, WorkflowIcon } from '~/components/icon/icon';
10 import { formatDate, formatFileSize, formatTime } from '~/common/formatters';
11 import { resourceLabel } from '~/common/labels';
12 import { connect, DispatchProp } from 'react-redux';
13 import { RootState } from '~/store/store';
14 import { getResource } from '~/store/resources/resources';
15 import { GroupContentsResource } from '~/services/groups-service/groups-service';
16 import { getProcess, Process, getProcessStatus, getProcessStatusColor, getProcessRuntime } from '~/store/processes/process';
17 import { ArvadosTheme } from '~/common/custom-theme';
18 import { compose, Dispatch } from 'redux';
19 import { WorkflowResource } from '~/models/workflow';
20 import { ResourceStatus as WorkflowStatus } from '~/views/workflow-panel/workflow-panel-view';
21 import { getUuidPrefix, openRunProcess } from '~/store/workflow-panel/workflow-panel-actions';
22 import { openSharingDialog } from '~/store/sharing-dialog/sharing-dialog-actions';
23 import { getUserFullname, User, UserResource } from '~/models/user';
24 import { toggleIsActive, toggleIsAdmin } from '~/store/users/users-actions';
25 import { LinkResource } from '~/models/link';
26 import { navigateTo } from '~/store/navigation/navigation-action';
27 import { withResourceData } from '~/views-components/data-explorer/with-resources';
28 import { CollectionResource } from '~/models/collection';
29 import { IllegalNamingWarning } from '~/components/warning/warning';
30 import { loadResource } from '~/store/resources/resources-actions';
31
32 const renderName = (dispatch: Dispatch, item: GroupContentsResource) =>
33     <Grid container alignItems="center" wrap="nowrap" spacing={16}>
34         <Grid item>
35             {renderIcon(item)}
36         </Grid>
37         <Grid item>
38             <Typography color="primary" style={{ width: 'auto', cursor: 'pointer' }} onClick={() => dispatch<any>(navigateTo(item.uuid))}>
39                 {item.kind === ResourceKind.PROJECT || item.kind === ResourceKind.COLLECTION
40                     ? <IllegalNamingWarning name={item.name} />
41                     : null}
42                 {item.name}
43             </Typography>
44         </Grid>
45         <Grid item>
46             <Typography variant="caption">
47                 <FavoriteStar resourceUuid={item.uuid} />
48                 <PublicFavoriteStar resourceUuid={item.uuid} />
49             </Typography>
50         </Grid>
51     </Grid>;
52
53 export const ResourceName = connect(
54     (state: RootState, props: { uuid: string }) => {
55         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
56         return resource;
57     })((resource: GroupContentsResource & DispatchProp<any>) => renderName(resource.dispatch, resource));
58
59 const renderIcon = (item: GroupContentsResource) => {
60     switch (item.kind) {
61         case ResourceKind.PROJECT:
62             return <ProjectIcon />;
63         case ResourceKind.COLLECTION:
64             if (item.uuid === item.currentVersionUuid) {
65                 return <CollectionIcon />;
66             }
67             return <CollectionOldVersionIcon />;
68         case ResourceKind.PROCESS:
69             return <ProcessIcon />;
70         case ResourceKind.WORKFLOW:
71             return <WorkflowIcon />;
72         default:
73             return <DefaultIcon />;
74     }
75 };
76
77 const renderDate = (date?: string) => {
78     return <Typography noWrap style={{ minWidth: '100px' }}>{formatDate(date)}</Typography>;
79 };
80
81 const renderWorkflowName = (item: WorkflowResource) =>
82     <Grid container alignItems="center" wrap="nowrap" spacing={16}>
83         <Grid item>
84             {renderIcon(item)}
85         </Grid>
86         <Grid item>
87             <Typography color="primary" style={{ width: '100px' }}>
88                 {item.name}
89             </Typography>
90         </Grid>
91     </Grid>;
92
93 export const ResourceWorkflowName = connect(
94     (state: RootState, props: { uuid: string }) => {
95         const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
96         return resource;
97     })(renderWorkflowName);
98
99 const getPublicUuid = (uuidPrefix: string) => {
100     return `${uuidPrefix}-tpzed-anonymouspublic`;
101 };
102
103 const resourceShare = (dispatch: Dispatch, uuidPrefix: string, ownerUuid?: string, uuid?: string) => {
104     const isPublic = ownerUuid === getPublicUuid(uuidPrefix);
105     return (
106         <div>
107             {!isPublic && uuid &&
108                 <Tooltip title="Share">
109                     <IconButton onClick={() => dispatch<any>(openSharingDialog(uuid))}>
110                         <ShareIcon />
111                     </IconButton>
112                 </Tooltip>
113             }
114         </div>
115     );
116 };
117
118 export const ResourceShare = connect(
119     (state: RootState, props: { uuid: string }) => {
120         const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
121         const uuidPrefix = getUuidPrefix(state);
122         return {
123             uuid: resource ? resource.uuid : '',
124             ownerUuid: resource ? resource.ownerUuid : '',
125             uuidPrefix
126         };
127     })((props: { ownerUuid?: string, uuidPrefix: string, uuid?: string } & DispatchProp<any>) =>
128         resourceShare(props.dispatch, props.uuidPrefix, props.ownerUuid, props.uuid));
129
130 const renderFirstName = (item: { firstName: string }) => {
131     return <Typography noWrap>{item.firstName}</Typography>;
132 };
133
134 // User Resources
135 export const ResourceFirstName = connect(
136     (state: RootState, props: { uuid: string }) => {
137         const resource = getResource<UserResource>(props.uuid)(state.resources);
138         return resource || { firstName: '' };
139     })(renderFirstName);
140
141 const renderLastName = (item: { lastName: string }) =>
142     <Typography noWrap>{item.lastName}</Typography>;
143
144 export const ResourceLastName = connect(
145     (state: RootState, props: { uuid: string }) => {
146         const resource = getResource<UserResource>(props.uuid)(state.resources);
147         return resource || { lastName: '' };
148     })(renderLastName);
149
150 const renderUuid = (item: { uuid: string }) =>
151     <Typography noWrap>{item.uuid}</Typography>;
152
153 export const ResourceUuid = connect(
154     (state: RootState, props: { uuid: string }) => {
155         const resource = getResource<UserResource>(props.uuid)(state.resources);
156         return resource || { uuid: '' };
157     })(renderUuid);
158
159 const renderEmail = (item: { email: string }) =>
160     <Typography noWrap>{item.email}</Typography>;
161
162 export const ResourceEmail = connect(
163     (state: RootState, props: { uuid: string }) => {
164         const resource = getResource<UserResource>(props.uuid)(state.resources);
165         return resource || { email: '' };
166     })(renderEmail);
167
168 const renderIsActive = (props: { uuid: string, isActive: boolean, toggleIsActive: (uuid: string) => void }) =>
169     <Checkbox
170         color="primary"
171         checked={props.isActive}
172         onClick={() => props.toggleIsActive(props.uuid)} />;
173
174 export const ResourceIsActive = connect(
175     (state: RootState, props: { uuid: string }) => {
176         const resource = getResource<UserResource>(props.uuid)(state.resources);
177         return resource || { isActive: false };
178     }, { toggleIsActive }
179 )(renderIsActive);
180
181 const renderIsAdmin = (props: { uuid: string, isAdmin: boolean, toggleIsAdmin: (uuid: string) => void }) =>
182     <Checkbox
183         color="primary"
184         checked={props.isAdmin}
185         onClick={() => props.toggleIsAdmin(props.uuid)} />;
186
187 export const ResourceIsAdmin = connect(
188     (state: RootState, props: { uuid: string }) => {
189         const resource = getResource<UserResource>(props.uuid)(state.resources);
190         return resource || { isAdmin: false };
191     }, { toggleIsAdmin }
192 )(renderIsAdmin);
193
194 const renderUsername = (item: { username: string }) =>
195     <Typography noWrap>{item.username}</Typography>;
196
197 export const ResourceUsername = connect(
198     (state: RootState, props: { uuid: string }) => {
199         const resource = getResource<UserResource>(props.uuid)(state.resources);
200         return resource || { username: '' };
201     })(renderUsername);
202
203 // Common methods
204 const renderCommonData = (data: string) =>
205     <Typography noWrap>{data}</Typography>;
206
207 const renderCommonDate = (date: string) =>
208     <Typography noWrap>{formatDate(date)}</Typography>;
209
210 export const CommonUuid = withResourceData('uuid', renderCommonData);
211
212 // Api Client Authorizations
213 export const TokenApiClientId = withResourceData('apiClientId', renderCommonData);
214
215 export const TokenApiToken = withResourceData('apiToken', renderCommonData);
216
217 export const TokenCreatedByIpAddress = withResourceData('createdByIpAddress', renderCommonDate);
218
219 export const TokenDefaultOwnerUuid = withResourceData('defaultOwnerUuid', renderCommonData);
220
221 export const TokenExpiresAt = withResourceData('expiresAt', renderCommonDate);
222
223 export const TokenLastUsedAt = withResourceData('lastUsedAt', renderCommonDate);
224
225 export const TokenLastUsedByIpAddress = withResourceData('lastUsedByIpAddress', renderCommonData);
226
227 export const TokenScopes = withResourceData('scopes', renderCommonData);
228
229 export const TokenUserId = withResourceData('userId', renderCommonData);
230
231 // Compute Node Resources
232 const renderNodeInfo = (data: string) => {
233     return <Typography>{JSON.stringify(data, null, 4)}</Typography>;
234 };
235
236 const clusterColors = [
237     ['#f44336', '#fff'],
238     ['#2196f3', '#fff'],
239     ['#009688', '#fff'],
240     ['#cddc39', '#fff'],
241     ['#ff9800', '#fff']
242 ];
243
244 export const ResourceCluster = (props: { uuid: string }) => {
245     const CLUSTER_ID_LENGTH = 5;
246     const pos = props.uuid.length > CLUSTER_ID_LENGTH ? props.uuid.indexOf('-') : 5;
247     const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substr(0, pos) : '';
248     const ci = pos >= CLUSTER_ID_LENGTH ? (((((
249         (props.uuid.charCodeAt(0) * props.uuid.charCodeAt(1))
250         + props.uuid.charCodeAt(2))
251         * props.uuid.charCodeAt(3))
252         + props.uuid.charCodeAt(4))) % clusterColors.length) : 0;
253     return <span style={{
254         backgroundColor: clusterColors[ci][0],
255         color: clusterColors[ci][1],
256         padding: "2px 7px",
257         borderRadius: 3
258     }}>{clusterId}</span>;
259 };
260
261 export const ComputeNodeInfo = withResourceData('info', renderNodeInfo);
262
263 export const ComputeNodeDomain = withResourceData('domain', renderCommonData);
264
265 export const ComputeNodeFirstPingAt = withResourceData('firstPingAt', renderCommonDate);
266
267 export const ComputeNodeHostname = withResourceData('hostname', renderCommonData);
268
269 export const ComputeNodeIpAddress = withResourceData('ipAddress', renderCommonData);
270
271 export const ComputeNodeJobUuid = withResourceData('jobUuid', renderCommonData);
272
273 export const ComputeNodeLastPingAt = withResourceData('lastPingAt', renderCommonDate);
274
275 // Links Resources
276 const renderLinkName = (item: { name: string }) =>
277     <Typography noWrap>{item.name || '(none)'}</Typography>;
278
279 export const ResourceLinkName = connect(
280     (state: RootState, props: { uuid: string }) => {
281         const resource = getResource<LinkResource>(props.uuid)(state.resources);
282         return resource || { name: '' };
283     })(renderLinkName);
284
285 const renderLinkClass = (item: { linkClass: string }) =>
286     <Typography noWrap>{item.linkClass}</Typography>;
287
288 export const ResourceLinkClass = connect(
289     (state: RootState, props: { uuid: string }) => {
290         const resource = getResource<LinkResource>(props.uuid)(state.resources);
291         return resource || { linkClass: '' };
292     })(renderLinkClass);
293
294 const renderLinkTail = (dispatch: Dispatch, item: { uuid: string, tailUuid: string, tailKind: string }) => {
295     const currentLabel = resourceLabel(item.tailKind);
296     const isUnknow = currentLabel === "Unknown";
297     return (<div>
298         {!isUnknow ? (
299             renderLink(dispatch, item.tailUuid, currentLabel)
300         ) : (
301                 <Typography noWrap color="default">
302                     {item.tailUuid}
303                 </Typography>
304             )}
305     </div>);
306 };
307
308 const renderLink = (dispatch: Dispatch, uuid: string, label: string) =>
309     <Typography noWrap color="primary" style={{ 'cursor': 'pointer' }} onClick={() => dispatch<any>(navigateTo(uuid))}>
310         {label}: {uuid}
311     </Typography>;
312
313 export const ResourceLinkTail = connect(
314     (state: RootState, props: { uuid: string }) => {
315         const resource = getResource<LinkResource>(props.uuid)(state.resources);
316         return {
317             item: resource || { uuid: '', tailUuid: '', tailKind: ResourceKind.NONE }
318         };
319     })((props: { item: any } & DispatchProp<any>) =>
320         renderLinkTail(props.dispatch, props.item));
321
322 const renderLinkHead = (dispatch: Dispatch, item: { uuid: string, headUuid: string, headKind: ResourceKind }) =>
323     renderLink(dispatch, item.headUuid, resourceLabel(item.headKind));
324
325 export const ResourceLinkHead = connect(
326     (state: RootState, props: { uuid: string }) => {
327         const resource = getResource<LinkResource>(props.uuid)(state.resources);
328         return {
329             item: resource || { uuid: '', headUuid: '', headKind: ResourceKind.NONE }
330         };
331     })((props: { item: any } & DispatchProp<any>) =>
332         renderLinkHead(props.dispatch, props.item));
333
334 export const ResourceLinkUuid = connect(
335     (state: RootState, props: { uuid: string }) => {
336         const resource = getResource<LinkResource>(props.uuid)(state.resources);
337         return resource || { uuid: '' };
338     })(renderUuid);
339
340 // Process Resources
341 const resourceRunProcess = (dispatch: Dispatch, uuid: string) => {
342     return (
343         <div>
344             {uuid &&
345                 <Tooltip title="Run process">
346                     <IconButton onClick={() => dispatch<any>(openRunProcess(uuid))}>
347                         <ProcessIcon />
348                     </IconButton>
349                 </Tooltip>}
350         </div>
351     );
352 };
353
354 export const ResourceRunProcess = connect(
355     (state: RootState, props: { uuid: string }) => {
356         const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
357         return {
358             uuid: resource ? resource.uuid : ''
359         };
360     })((props: { uuid: string } & DispatchProp<any>) =>
361         resourceRunProcess(props.dispatch, props.uuid));
362
363 const renderWorkflowStatus = (uuidPrefix: string, ownerUuid?: string) => {
364     if (ownerUuid === getPublicUuid(uuidPrefix)) {
365         return renderStatus(WorkflowStatus.PUBLIC);
366     } else {
367         return renderStatus(WorkflowStatus.PRIVATE);
368     }
369 };
370
371 const renderStatus = (status: string) =>
372     <Typography noWrap style={{ width: '60px' }}>{status}</Typography>;
373
374 export const ResourceWorkflowStatus = connect(
375     (state: RootState, props: { uuid: string }) => {
376         const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
377         const uuidPrefix = getUuidPrefix(state);
378         return {
379             ownerUuid: resource ? resource.ownerUuid : '',
380             uuidPrefix
381         };
382     })((props: { ownerUuid?: string, uuidPrefix: string }) => renderWorkflowStatus(props.uuidPrefix, props.ownerUuid));
383
384 export const ResourceLastModifiedDate = connect(
385     (state: RootState, props: { uuid: string }) => {
386         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
387         return { date: resource ? resource.modifiedAt : '' };
388     })((props: { date: string }) => renderDate(props.date));
389
390 export const ResourceCreatedAtDate = connect(
391     (state: RootState, props: { uuid: string }) => {
392         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
393         return { date: resource ? resource.createdAt : '' };
394     })((props: { date: string }) => renderDate(props.date));
395
396 export const ResourceTrashDate = connect(
397     (state: RootState, props: { uuid: string }) => {
398         const resource = getResource<TrashableResource>(props.uuid)(state.resources);
399         return { date: resource ? resource.trashAt : '' };
400     })((props: { date: string }) => renderDate(props.date));
401
402 export const ResourceDeleteDate = connect(
403     (state: RootState, props: { uuid: string }) => {
404         const resource = getResource<TrashableResource>(props.uuid)(state.resources);
405         return { date: resource ? resource.deleteAt : '' };
406     })((props: { date: string }) => renderDate(props.date));
407
408 export const renderFileSize = (fileSize?: number) =>
409     <Typography noWrap style={{ minWidth: '45px' }}>
410         {formatFileSize(fileSize)}
411     </Typography>;
412
413 export const ResourceFileSize = connect(
414     (state: RootState, props: { uuid: string }) => {
415         const resource = getResource<CollectionResource>(props.uuid)(state.resources);
416         return { fileSize: resource ? resource.fileSizeTotal : 0 };
417     })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
418
419 const renderOwner = (owner: string) =>
420     <Typography noWrap>
421         {owner}
422     </Typography>;
423
424 export const ResourceOwner = connect(
425     (state: RootState, props: { uuid: string }) => {
426         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
427         return { owner: resource ? resource.ownerUuid : '' };
428     })((props: { owner: string }) => renderOwner(props.owner));
429
430 export const ResourceOwnerName = connect(
431     (state: RootState, props: { uuid: string }) => {
432         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
433         const ownerNameState = state.ownerName;
434         const ownerName = ownerNameState.find(it => it.uuid === resource!.ownerUuid);
435         return { owner: ownerName ? ownerName!.name : resource!.ownerUuid };
436     })((props: { owner: string }) => renderOwner(props.owner));
437
438 export const ResourceOwnerWithName =
439     compose(
440         connect(
441             (state: RootState, props: { uuid: string }) => {
442                 let ownerName = '';
443                 const resource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
444
445                 if (resource) {
446                     ownerName = getUserFullname(resource as User) || (resource as GroupContentsResource).name;
447                 }
448
449                 return { uuid: props.uuid, ownerName };
450             }),
451         withStyles({}, { withTheme: true }))
452         ((props: { uuid: string, ownerName: string, dispatch: Dispatch, theme: ArvadosTheme }) => {
453             const { uuid, ownerName, dispatch, theme } = props;
454
455             if (ownerName === '') {
456                 dispatch<any>(loadResource(uuid, false));
457                 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
458                     {uuid}
459                 </Typography>;
460             }
461
462             return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
463                 {ownerName} ({uuid})
464             </Typography>;
465         });
466
467 const renderType = (type: string) =>
468     <Typography noWrap>
469         {resourceLabel(type)}
470     </Typography>;
471
472 export const ResourceType = connect(
473     (state: RootState, props: { uuid: string }) => {
474         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
475         return { type: resource ? resource.kind : '' };
476     })((props: { type: string }) => renderType(props.type));
477
478 export const ResourceStatus = connect((state: RootState, props: { uuid: string }) => {
479     return { resource: getResource<GroupContentsResource>(props.uuid)(state.resources) };
480 })((props: { resource: GroupContentsResource }) =>
481     (props.resource && props.resource.kind === ResourceKind.COLLECTION)
482         ? <CollectionStatus uuid={props.resource.uuid} />
483         : <ProcessStatus uuid={props.resource.uuid} />
484 );
485
486 export const CollectionStatus = connect((state: RootState, props: { uuid: string }) => {
487     return { collection: getResource<CollectionResource>(props.uuid)(state.resources) };
488 })((props: { collection: CollectionResource }) =>
489     (props.collection.uuid !== props.collection.currentVersionUuid)
490         ? <Typography>version {props.collection.version}</Typography>
491         : <Typography>head version</Typography>
492 );
493
494 export const ProcessStatus = compose(
495     connect((state: RootState, props: { uuid: string }) => {
496         return { process: getProcess(props.uuid)(state.resources) };
497     }),
498     withStyles({}, { withTheme: true }))
499     ((props: { process?: Process, theme: ArvadosTheme }) => {
500         const status = props.process ? getProcessStatus(props.process) : "-";
501         return <Typography
502             noWrap
503             style={{ color: getProcessStatusColor(status, props.theme) }} >
504             {status}
505         </Typography>;
506     });
507
508 export const ProcessStartDate = connect(
509     (state: RootState, props: { uuid: string }) => {
510         const process = getProcess(props.uuid)(state.resources);
511         return { date: (process && process.container) ? process.container.startedAt : '' };
512     })((props: { date: string }) => renderDate(props.date));
513
514 export const renderRunTime = (time: number) =>
515     <Typography noWrap style={{ minWidth: '45px' }}>
516         {formatTime(time, true)}
517     </Typography>;
518
519 interface ContainerRunTimeProps {
520     process: Process;
521 }
522
523 interface ContainerRunTimeState {
524     runtime: number;
525 }
526
527 export const ContainerRunTime = connect((state: RootState, props: { uuid: string }) => {
528     return { process: getProcess(props.uuid)(state.resources) };
529 })(class extends React.Component<ContainerRunTimeProps, ContainerRunTimeState> {
530     private timer: any;
531
532     constructor(props: ContainerRunTimeProps) {
533         super(props);
534         this.state = { runtime: this.getRuntime() };
535     }
536
537     getRuntime() {
538         return this.props.process ? getProcessRuntime(this.props.process) : 0;
539     }
540
541     updateRuntime() {
542         this.setState({ runtime: this.getRuntime() });
543     }
544
545     componentDidMount() {
546         this.timer = setInterval(this.updateRuntime.bind(this), 5000);
547     }
548
549     componentWillUnmount() {
550         clearInterval(this.timer);
551     }
552
553     render() {
554         return renderRunTime(this.state.runtime);
555     }
556 });