diff --git a/applications/client/src/store/campaign/timeline.ts b/applications/client/src/store/campaign/timeline.ts index f3ca2a99..4f7bccb7 100644 --- a/applications/client/src/store/campaign/timeline.ts +++ b/applications/client/src/store/campaign/timeline.ts @@ -116,6 +116,17 @@ export class TimelineStore extends ExtendedModel(() => ({ pair?.beaconId && selectedBeaconIds.includes(pair?.beaconId) ? sum + (pair?.commandCount ?? 0) : sum, 0 ), + beaconCommands: bucket?.beaconCommandCountPair.reduce( + (commands: Array>, pair) => [ + ...commands, + { + beaconId: pair?.beaconId, + commandCount: pair?.commandCount, + }, + ], + [] + ), + beaconNumbers: bucket?.beaconCommandCountPair.length, })) ?? [] ); } diff --git a/applications/client/src/views/Campaign/Explore/Panels/Command/CommandContainer.tsx b/applications/client/src/views/Campaign/Explore/Panels/Command/CommandContainer.tsx index d2e88a23..bbd66225 100644 --- a/applications/client/src/views/Campaign/Explore/Panels/Command/CommandContainer.tsx +++ b/applications/client/src/views/Campaign/Explore/Panels/Command/CommandContainer.tsx @@ -23,7 +23,7 @@ type CommandContainerProps = ComponentProps<'div'> & { hideCommentButton?: boolean; showPath?: boolean; expandedCommandIDs?: string[]; - removeExplandedCommandID?: (commandId: string) => void; + removeExpandedCommandID?: (commandId: string) => void; }; export const CommandContainer = observer( @@ -36,7 +36,7 @@ export const CommandContainer = observer( hideCommentButton = false, showPath = false, expandedCommandIDs = [], - removeExplandedCommandID, + removeExpandedCommandID, ...props }) => { const store = useStore(); @@ -68,7 +68,7 @@ export const CommandContainer = observer( }, }); } - removeExplandedCommandID?.(state.commandId); + removeExpandedCommandID?.(state.commandId); } }, localCommand: undefined as undefined | CommandModel, diff --git a/applications/client/src/views/Campaign/Explore/Panels/Command/Commands.tsx b/applications/client/src/views/Campaign/Explore/Panels/Command/Commands.tsx index de6456fb..5909a319 100644 --- a/applications/client/src/views/Campaign/Explore/Panels/Command/Commands.tsx +++ b/applications/client/src/views/Campaign/Explore/Panels/Command/Commands.tsx @@ -27,7 +27,7 @@ export const Commands = observer(({ sort, showPath = true }) => { expandedCommandIDs: store.router.params.activeItemId ? observable.array([store.router.params.activeItemId]) : observable.array([]), - removeExplandedCommandID(commandId: string) { + removeExpandedCommandID(commandId: string) { this.expandedCommandIDs.remove(commandId); }, scrollToCommand(commandId: string, commandIds: string[], behavior: ScrollBehavior = 'smooth') { @@ -125,7 +125,7 @@ export const Commands = observer(({ sort, showPath = true }) => { data-command-id={commandId} showPath={showPath} expandedCommandIDs={state.expandedCommandIDs} - removeExplandedCommandID={state.removeExplandedCommandID} + removeExpandedCommandID={state.removeExpandedCommandID} /> )) )} diff --git a/applications/client/src/views/Campaign/Timeline/BarLabels.tsx b/applications/client/src/views/Campaign/Timeline/BarLabels.tsx new file mode 100644 index 00000000..826d4d8d --- /dev/null +++ b/applications/client/src/views/Campaign/Timeline/BarLabels.tsx @@ -0,0 +1,122 @@ +import { css } from '@emotion/react'; +import { dateFormat, dateTimeFormat, Flex } from '@redeye/client/components'; +import { routes, useStore } from '@redeye/client/store'; +import { CampaignViews, Tabs } from '@redeye/client/types'; +import type { UUID } from '@redeye/client/types'; +import { Txt, FlexSplitter, Tokens } from '@redeye/ui-styles'; +import { observer } from 'mobx-react-lite'; +import type { ComponentProps } from 'react'; +import type { IBar } from './TimelineChart'; + +type BarLabelsProps = ComponentProps<'div'> & { + bar: IBar; + dateFormatter: string | undefined; +}; + +export const BarLabelDate = observer(({ bar, dateFormatter }) => { + const store = useStore(); + const dateStart = store.settings.momentTz(bar?.start).format(dateFormatter); + const dateEnd = store.settings.momentTz(bar?.end).format(dateFormatter); + const sameDate = dateStart.split(' ')[0] === dateEnd.split(' ')[0]; + + return sameDate && dateFormatter === dateFormat ? ( + + {dateStart} + + ) : sameDate && dateFormatter === dateTimeFormat ? ( + {`${dateStart} - ${dateEnd.split(' ')[1]}`} + ) : ( + {`${dateStart} - ${dateEnd}`} + ); +}); + +export const BarLabelOnHover = observer(({ bar, dateFormatter }) => ( +
+ + + + + Beacons + + + {bar?.beaconNumbers} + + + + Total commands + + + {bar?.beaconCount} + + + + Active Beacon commands + + + {bar?.activeBeaconCount} + +
+)); + +export const BarLabelBeaconList = observer(({ bar, dateFormatter }) => { + const store = useStore(); + const routeToBeacon = (beaconId: string) => { + store.router.updateRoute({ + path: routes[CampaignViews.EXPLORE], + params: { + view: CampaignViews.EXPLORE, + currentItem: 'beacon', + currentItemId: beaconId as UUID, + tab: Tabs.COMMANDS, + activeItem: undefined, + activeItemId: undefined, + }, + }); + }; + return ( +
+ + + + + Beacons + + + + Commands + + + {bar.beaconCommands.map((beaconCommand) => ( + routeToBeacon(beaconCommand.beaconId as string)} + > + + {store.graphqlStore.beacons.get(beaconCommand.beaconId as string)?.displayName} + + + {store.graphqlStore.beacons.get(beaconCommand.beaconId as string)?.meta[0].maybeCurrent?.username} + + + {beaconCommand.commandCount} + + ))} +
+ ); +}); + +const barPopoverStyles = css` + padding: 0.4rem; +`; + +const marginStyles = (num: number) => css` + margin-right: ${num}rem; +`; + +const barPopoverRowStyles = css` + &:hover { + cursor: pointer; + background: ${Tokens.Components.MinimalButtonBackgroundColorHover}; + } +`; diff --git a/applications/client/src/views/Campaign/Timeline/Bars.tsx b/applications/client/src/views/Campaign/Timeline/Bars.tsx index 23ef1e91..9a4f5c5f 100644 --- a/applications/client/src/views/Campaign/Timeline/Bars.tsx +++ b/applications/client/src/views/Campaign/Timeline/Bars.tsx @@ -1,10 +1,12 @@ +import { Popover2, Popover2InteractionKind } from '@blueprintjs/popover2'; import { css } from '@emotion/react'; +import { createState, durationFormatter } from '@redeye/client/components'; import { Tokens } from '@redeye/ui-styles'; import { max, scaleLinear } from 'd3'; import { observer } from 'mobx-react-lite'; import type { ComponentProps } from 'react'; -import { Fragment } from 'react'; import { animated } from 'react-spring'; +import { BarLabelOnHover, BarLabelBeaconList } from './BarLabels'; import { TIMELINE_BG_COLOR } from './timeline-static-vars'; import type { IBar, IDimensions, TimeScale } from './TimelineChart'; @@ -17,9 +19,15 @@ type BarsProps = ComponentProps<'div'> & { scrubberTime: Date | null; }; -export const Bars = observer(({ xScale, bars, start, dimensions, scrubberTime }) => { +export const Bars = observer(({ xScale, bars, start, end, dimensions, scrubberTime }) => { const yMax = max(bars.map((bar) => bar.beaconCount)) ?? 0; const yScale = scaleLinear([0, yMax], [0, dimensions.height]); + const state = createState({ + isHover: true as boolean, + toggleIsHover() { + this.isHover = !this.isHover; + }, + }); return ( @@ -28,32 +36,78 @@ export const Bars = observer(({ xScale, bars, start, dimensions, scru const width = xScale(bar.end) - x; return ( - - {/* Dead & Future Beacon Bar */} - - {/* Active Beacon Bar */} - - {/* Selected Beacon Bar */} - - + + ) : ( + + ) + ) : undefined + } + placement="bottom" + modifiers={{ + arrow: { enabled: false }, + offset: { + enabled: true, + options: { + offset: [0, 20], + }, + }, + }} + renderTarget={({ isOpen, ref, ...targetProps }) => ( + { + e.preventDefault(); + state.toggleIsHover(); + }} + > + {/* Interaction Beacon Bar for color */} + {bar.beaconCount && ( + + )} + {/* Dead & Future Beacon Bar */} + + {/* Active Beacon Bar */} + + {/* Selected Beacon Bar */} + + {/* Interaction Beacon Bar for Functionality */} + {!!bar.beaconCount && ( + + )} + + )} + /> ); })} @@ -81,3 +135,13 @@ const aliveBarStyles = css` const selectedBarStyles = css` fill: ${Tokens.CoreTokens.BeaconSelected}; `; + +const interactionBarStyles = (hover: boolean) => css` + fill: ${hover ? Tokens.CoreTokens.BeaconInteracted : 'transparent'}; + opacity: 0.3; +`; + +const interactionBarFnStyles = css` + fill: transparent; + cursor: pointer; +`; diff --git a/applications/client/src/views/Campaign/Timeline/TimelineChart.tsx b/applications/client/src/views/Campaign/Timeline/TimelineChart.tsx index 88d78cc7..4d85063f 100644 --- a/applications/client/src/views/Campaign/Timeline/TimelineChart.tsx +++ b/applications/client/src/views/Campaign/Timeline/TimelineChart.tsx @@ -23,6 +23,8 @@ export interface IBar { beaconCount: number; activeBeaconCount: number; selectedBeaconCount: number; + beaconNumbers: number; + beaconCommands: Array>; } export interface IDimensions { diff --git a/packages/client/ui-styles/src/tokens.ts b/packages/client/ui-styles/src/tokens.ts index dd13a82c..d8322d63 100644 --- a/packages/client/ui-styles/src/tokens.ts +++ b/packages/client/ui-styles/src/tokens.ts @@ -24,6 +24,7 @@ const CoreTokens = { BeaconAlive: BpTokens.Colors.Gray2, BeaconFuture: BpTokens.Colors.Black, BeaconSelected: BpTokens.Colors.White, + BeaconInteracted: BpTokens.Colors.Black, FontWeightBold: '700', FontWeightNormal: '400',