Skip to content
This repository has been archived by the owner on Oct 20, 2023. It is now read-only.

Feature/343 timeline hover bar to see beacon data #60

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions applications/client/src/store/campaign/timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, string | number>>, pair) => [
...commands,
{
beaconId: pair?.beaconId,
commandCount: pair?.commandCount,
},
],
[]
),
beaconNumbers: bucket?.beaconCommandCountPair.length,
})) ?? []
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CommandContainerProps>(
Expand All @@ -36,7 +36,7 @@ export const CommandContainer = observer<CommandContainerProps>(
hideCommentButton = false,
showPath = false,
expandedCommandIDs = [],
removeExplandedCommandID,
removeExpandedCommandID,
...props
}) => {
const store = useStore();
Expand Down Expand Up @@ -68,7 +68,7 @@ export const CommandContainer = observer<CommandContainerProps>(
},
});
}
removeExplandedCommandID?.(state.commandId);
removeExpandedCommandID?.(state.commandId);
}
},
localCommand: undefined as undefined | CommandModel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const Commands = observer<CommandsProps>(({ sort, showPath = true }) => {
expandedCommandIDs: store.router.params.activeItemId
? observable.array([store.router.params.activeItemId])
: observable.array<string>([]),
removeExplandedCommandID(commandId: string) {
removeExpandedCommandID(commandId: string) {
this.expandedCommandIDs.remove(commandId);
},
scrollToCommand(commandId: string, commandIds: string[], behavior: ScrollBehavior = 'smooth') {
Expand Down Expand Up @@ -125,7 +125,7 @@ export const Commands = observer<CommandsProps>(({ sort, showPath = true }) => {
data-command-id={commandId}
showPath={showPath}
expandedCommandIDs={state.expandedCommandIDs}
removeExplandedCommandID={state.removeExplandedCommandID}
removeExpandedCommandID={state.removeExpandedCommandID}
/>
))
)}
Expand Down
122 changes: 122 additions & 0 deletions applications/client/src/views/Campaign/Timeline/BarLabels.tsx
Original file line number Diff line number Diff line change
@@ -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<BarLabelsProps>(({ 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 ? (
<Txt block bold small>
{dateStart}
</Txt>
) : sameDate && dateFormatter === dateTimeFormat ? (
<Txt block bold small>{`${dateStart} - ${dateEnd.split(' ')[1]}`}</Txt>
) : (
<Txt block bold small>{`${dateStart} - ${dateEnd}`}</Txt>
);
});

export const BarLabelOnHover = observer<BarLabelsProps>(({ bar, dateFormatter }) => (
<div css={barPopoverStyles}>
<BarLabelDate bar={bar} dateFormatter={dateFormatter} />
<FlexSplitter />
<Flex css={{ 'padding-top': '0.2rem' }}>
<Txt muted small css={marginStyles(1)}>
Beacons
</Txt>
<FlexSplitter />
<Txt small>{bar?.beaconNumbers}</Txt>
</Flex>
<Flex>
<Txt muted small css={marginStyles(1)}>
Total commands
</Txt>
<FlexSplitter />
<Txt small>{bar?.beaconCount}</Txt>
</Flex>
<Flex>
<Txt muted small css={marginStyles(1)}>
Active Beacon commands
</Txt>
<FlexSplitter />
<Txt small>{bar?.activeBeaconCount}</Txt>
</Flex>
</div>
));

export const BarLabelBeaconList = observer<BarLabelsProps>(({ 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 (
<div css={barPopoverStyles}>
<BarLabelDate bar={bar} dateFormatter={dateFormatter} />
<FlexSplitter />
<Flex css={{ padding: '0.2rem 0' }}>
<Txt small bold>
Beacons
</Txt>
<FlexSplitter />
<Txt small bold>
Commands
</Txt>
</Flex>
{bar.beaconCommands.map((beaconCommand) => (
<Flex
key={beaconCommand.beaconId}
css={barPopoverRowStyles}
onClick={() => routeToBeacon(beaconCommand.beaconId as string)}
>
<Txt small css={marginStyles(0.5)}>
{store.graphqlStore.beacons.get(beaconCommand.beaconId as string)?.displayName}
</Txt>
<Txt muted small css={marginStyles(4)}>
{store.graphqlStore.beacons.get(beaconCommand.beaconId as string)?.meta[0].maybeCurrent?.username}
</Txt>
<FlexSplitter />
<Txt small>{beaconCommand.commandCount}</Txt>
</Flex>
))}
</div>
);
});

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};
}
`;
120 changes: 92 additions & 28 deletions applications/client/src/views/Campaign/Timeline/Bars.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -17,9 +19,15 @@ type BarsProps = ComponentProps<'div'> & {
scrubberTime: Date | null;
};

export const Bars = observer<BarsProps>(({ xScale, bars, start, dimensions, scrubberTime }) => {
export const Bars = observer<BarsProps>(({ 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 (
<g>
Expand All @@ -28,32 +36,78 @@ export const Bars = observer<BarsProps>(({ xScale, bars, start, dimensions, scru
const width = xScale(bar.end) - x;

return (
<Fragment key={`${start.valueOf()}-${bar.start.valueOf()}`}>
{/* Dead & Future Beacon Bar */}
<rect
x={x}
width={width}
y={dimensions.height - yScale(bar.beaconCount)}
height={yScale(bar.beaconCount)}
css={[baseBarStyles, scrubberTime && bar.end <= scrubberTime ? deadBarStyles : futureBarStyles]}
/>
{/* Active Beacon Bar */}
<rect
x={x}
width={width}
y={dimensions.height - yScale(bar.activeBeaconCount)}
height={yScale(bar.activeBeaconCount)}
css={[baseBarStyles, aliveBarStyles]}
/>
{/* Selected Beacon Bar */}
<animated.rect
x={x}
width={width}
y={dimensions.height - yScale(bar.selectedBeaconCount)}
height={yScale(bar.selectedBeaconCount)}
css={[baseBarStyles, selectedBarStyles]}
/>
</Fragment>
<Popover2
key={`${start.valueOf()}-${bar.start.valueOf()}`}
interactionKind={Popover2InteractionKind.HOVER}
content={
bar.beaconCount ? (
state.isHover ? (
<BarLabelOnHover bar={bar} dateFormatter={durationFormatter(start, end)} />
) : (
<BarLabelBeaconList bar={bar} dateFormatter={durationFormatter(start, end)} />
)
) : undefined
}
placement="bottom"
modifiers={{
arrow: { enabled: false },
offset: {
enabled: true,
options: {
offset: [0, 20],
},
},
}}
renderTarget={({ isOpen, ref, ...targetProps }) => (
<g
ref={ref}
{...targetProps}
onMouseDown={(e) => {
e.preventDefault();
state.toggleIsHover();
}}
>
{/* Interaction Beacon Bar for color */}
{bar.beaconCount && (
<rect
x={x}
width={width}
y={0}
height={dimensions.height}
css={[baseBarStyles, interactionBarStyles(isOpen)]}
/>
)}
{/* Dead & Future Beacon Bar */}
<rect
x={x}
width={width}
y={dimensions.height - yScale(bar.beaconCount)}
height={yScale(bar.beaconCount)}
css={[baseBarStyles, scrubberTime && bar.end <= scrubberTime ? deadBarStyles : futureBarStyles]}
/>
{/* Active Beacon Bar */}
<rect
x={x}
width={width}
y={dimensions.height - yScale(bar.activeBeaconCount)}
height={yScale(bar.activeBeaconCount)}
css={[baseBarStyles, aliveBarStyles]}
/>
{/* Selected Beacon Bar */}
<animated.rect
x={x}
width={width}
y={dimensions.height - yScale(bar.selectedBeaconCount)}
height={yScale(bar.selectedBeaconCount)}
css={[baseBarStyles, selectedBarStyles]}
/>
{/* Interaction Beacon Bar for Functionality */}
{!!bar.beaconCount && (
<rect x={x} width={width} y={0} height={dimensions.height} css={[interactionBarFnStyles]} />
)}
</g>
)}
/>
);
})}
</g>
Expand Down Expand Up @@ -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;
`;
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export interface IBar {
beaconCount: number;
activeBeaconCount: number;
selectedBeaconCount: number;
beaconNumbers: number;
beaconCommands: Array<Record<string, string | number>>;
}

export interface IDimensions {
Expand Down
1 change: 1 addition & 0 deletions packages/client/ui-styles/src/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down