Mirror of https://github.com/roostorg/osprey github.com/roostorg/osprey
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 250 lines 8.0 kB view raw
1import * as React from 'react'; 2import { 3 UnorderedListOutlined, 4 ProfileOutlined, 5 SortAscendingOutlined, 6 SortDescendingOutlined, 7} from '@ant-design/icons'; 8import { Empty, List, Skeleton, Tooltip, Spin } from 'antd'; 9import classNames from 'classnames'; 10import { memoize } from 'lodash'; 11import { AutoSizer, List as VList, ListRowProps } from 'react-virtualized'; 12import shallow from 'zustand/shallow'; 13 14import { getFeatureLocations, getScanQueryResults } from '../../actions/EventActions'; 15import useApplicationConfigStore from '../../stores/ApplicationConfigStore'; 16import useQueryStore from '../../stores/QueryStore'; 17import { OspreyEvent, ScanQueryOrder } from '../../types/QueryTypes'; 18import OspreyButton from '../../uikit/OspreyButton'; 19import EventStreamIcon from '../../uikit/icons/EventStreamIcon'; 20import Panel from '../common/Panel'; 21import EventStreamCard from './EventStreamCard'; 22import FeatureSelectModal from './FeatureSelectModal'; 23 24import styles from './EventStream.module.css'; 25import { FeatureLocation } from '../../types/ConfigTypes'; 26 27const QUERY_ROW_LIMIT = 100; 28const HEADER_HEIGHT = 44; 29const ADDITIONAL_PADDING = 4; 30const ROW_HEIGHT = 44; 31const EMPTY_ROW_HEIGHT = 116; 32const NUM_COLUMNS = 3; 33 34const calculateCardHeight = (currentHeight: number, featureBlock: readonly string[], item: OspreyEvent) => { 35 const numFeatures = featureBlock.reduce( 36 (acc, feature) => acc + (Object.prototype.hasOwnProperty.call(item.extracted_features, feature) ? 1 : 0), 37 0 38 ); 39 const rows = Math.ceil(numFeatures / NUM_COLUMNS); 40 return currentHeight + rows * ROW_HEIGHT; 41}; 42 43const EventStream: React.FC = () => { 44 const [ 45 executedQuery, 46 sortOrder, 47 entityFeatureFilters, 48 applyIfQueryIsCurrent, 49 updateSortOrder, 50 customSummaryFeatures, 51 ] = useQueryStore( 52 (state) => [ 53 state.executedQuery, 54 state.sortOrder, 55 state.entityFeatureFilters, 56 state.applyIfQueryIsCurrent, 57 state.updateSortOrder, 58 state.customSummaryFeatures, 59 ], 60 shallow 61 ); 62 const defaultSummaryFeatures = useApplicationConfigStore((state) => state.defaultSummaryFeatures); 63 const vlistRef = React.useRef<VList>(null); 64 65 const [isLoading, setIsLoading] = React.useState(false); 66 const [eventStream, setEventStream] = React.useState<OspreyEvent[]>([]); 67 const [queryOffset, setQueryOffset] = React.useState<string | null>(null); 68 const [isListView, setIsListView] = React.useState(false); 69 const [featureLocations, setFeatureLocations] = React.useState<FeatureLocation[] | undefined>([]); 70 71 const handleScanQuery = React.useCallback( 72 async (currentEvents: OspreyEvent[] = [], newQueryOffset?: string) => { 73 setIsLoading(true); 74 const { events, offset } = await getScanQueryResults( 75 { ...executedQuery, entityFeatureFilters }, 76 sortOrder, 77 QUERY_ROW_LIMIT, 78 newQueryOffset 79 ); 80 81 applyIfQueryIsCurrent(executedQuery, () => { 82 setEventStream([...currentEvents, ...events]); 83 setQueryOffset(offset); 84 setIsLoading(false); 85 }); 86 }, 87 [executedQuery, sortOrder, entityFeatureFilters, applyIfQueryIsCurrent] 88 ); 89 90 React.useEffect(() => { 91 const { start, end } = executedQuery; 92 setEventStream([]); 93 setQueryOffset(null); 94 95 if (start !== '' && end !== '') { 96 // This should only be called if the query has changed, so reset local state 97 handleScanQuery(); 98 } 99 }, [handleScanQuery, executedQuery]); 100 101 React.useEffect(() => { 102 (async () => { 103 const { locations } = await getFeatureLocations(); 104 setFeatureLocations(locations); 105 })(); 106 }, []); 107 108 React.useEffect(() => { 109 vlistRef.current?.recomputeRowHeights(); 110 }, [customSummaryFeatures, isListView, vlistRef]); 111 112 const handlePaginatedScanQuery = () => { 113 if (eventStream.length === 0 || queryOffset == null || isLoading) return; 114 handleScanQuery(eventStream, queryOffset); 115 }; 116 117 const shouldLoadMoreResults = (index: number): boolean => { 118 return eventStream.length - index < 5; 119 }; 120 121 const getSummaryFeatures = React.useMemo( 122 () => 123 memoize((actionName: string) => 124 defaultSummaryFeatures.filter((f) => f.appliesTo(actionName)).map((f) => f.features) 125 ), 126 [defaultSummaryFeatures] 127 ); 128 129 const renderListRows = ({ key, index, style, isVisible }: ListRowProps) => { 130 if (isVisible && shouldLoadMoreResults(index)) { 131 handlePaginatedScanQuery(); 132 } 133 134 const item = eventStream[index]; 135 const features = 136 customSummaryFeatures == null ? getSummaryFeatures(item.extracted_features.ActionName) : [customSummaryFeatures]; 137 138 return ( 139 <List.Item 140 className={classNames(styles.listItemWrapper, { [styles.listView]: isListView })} 141 key={key} 142 style={style} 143 > 144 <Skeleton loading={isLoading} active title paragraph={{ rows: 5 }}> 145 <EventStreamCard 146 selectedFeatures={features} 147 featureLocations={featureLocations} 148 eventDetails={item} 149 isListView={isListView} 150 /> 151 </Skeleton> 152 </List.Item> 153 ); 154 }; 155 156 const getRowHeight = ({ index }: { index: number }): number => { 157 if (isListView) return HEADER_HEIGHT; 158 159 const features = 160 customSummaryFeatures == null 161 ? getSummaryFeatures(eventStream[index].extracted_features.ActionName) 162 : [customSummaryFeatures]; 163 164 if (!features.length) { 165 return EMPTY_ROW_HEIGHT; 166 } 167 168 return features.reduce( 169 (height: number, featureBlock: readonly string[]) => 170 calculateCardHeight(height, featureBlock, eventStream[index]), 171 HEADER_HEIGHT + ADDITIONAL_PADDING 172 ); 173 }; 174 175 const renderVirtualizedList = () => { 176 if (eventStream.length === 0) { 177 return isLoading ? ( 178 <Spin size="large" style={{ position: 'absolute', top: 40, left: 0, right: 0 }} /> 179 ) : ( 180 <Empty style={{ paddingTop: 40 }} /> 181 ); 182 } 183 184 return ( 185 <AutoSizer> 186 {({ width, height }) => ( 187 <VList 188 className={styles.virtualizedList} 189 width={width} 190 height={height} 191 rowCount={eventStream.length} 192 rowHeight={getRowHeight} 193 rowRenderer={renderListRows} 194 ref={vlistRef} 195 /> 196 )} 197 </AutoSizer> 198 ); 199 }; 200 201 const handleToggleViewType = () => { 202 setIsListView(!isListView); 203 }; 204 205 const handleSetQueryOrder = (order: ScanQueryOrder) => { 206 updateSortOrder(order); 207 }; 208 209 const renderTitleRight = () => { 210 const viewSwitchIcon = isListView ? <ProfileOutlined /> : <UnorderedListOutlined />; 211 const oppositeView = isListView ? 'card' : 'list'; 212 const oppositeOrder = sortOrder === ScanQueryOrder.ASCENDING ? ScanQueryOrder.DESCENDING : ScanQueryOrder.ASCENDING; 213 const orderIcon = sortOrder === ScanQueryOrder.ASCENDING ? <SortAscendingOutlined /> : <SortDescendingOutlined />; 214 215 return ( 216 <div className={styles.buttonContainer}> 217 <Tooltip title={`Switch to sorting by date ${oppositeOrder} (currently: ${sortOrder})`}> 218 <span> 219 <OspreyButton 220 className={styles.viewSwitchButton} 221 icon={orderIcon} 222 onClick={() => handleSetQueryOrder(oppositeOrder)} 223 /> 224 </span> 225 </Tooltip> 226 <Tooltip title={`Switch to ${oppositeView} view`}> 227 <span> 228 <OspreyButton className={styles.viewSwitchButton} icon={viewSwitchIcon} onClick={handleToggleViewType} /> 229 </span> 230 </Tooltip> 231 <FeatureSelectModal /> 232 </div> 233 ); 234 }; 235 236 return ( 237 <Panel 238 className={styles.eventStreamPanel} 239 title="Event Stream" 240 titleRight={renderTitleRight()} 241 icon={<EventStreamIcon />} 242 > 243 <div className={styles.listWrapper}> 244 <div className={styles.eventStreamList}>{renderVirtualizedList()}</div> 245 </div> 246 </Panel> 247 ); 248}; 249 250export default EventStream;