Skip to content

Commit b5d771f

Browse files
Allow deep-linking to specific history events (#905)
* Expand the selected event when first loading page * Highlight event groups on both timeline and compact view even when the selected event is a non-start event * Fix timeline event header styling so that it's no longer necessary for the title div to span the entire screen width * Add a link copy button to expanded event titles that copies the link to a specific event
1 parent ebd374f commit b5d771f

File tree

4 files changed

+148
-24
lines changed

4 files changed

+148
-24
lines changed

src/views/workflow-history/workflow-history-events-card/__tests__/workflow-history-events-card.test.tsx

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import copy from 'copy-to-clipboard';
2+
13
import { render, screen, userEvent } from '@/test-utils/rtl';
24

35
import {
@@ -17,6 +19,8 @@ jest.mock(
1719
() => jest.fn(({ event }) => <div>Details eventId: {event.eventId}</div>)
1820
);
1921

22+
jest.mock('copy-to-clipboard', jest.fn);
23+
2024
describe('WorkflowHistoryEventsCard', () => {
2125
it('shows events label and status correctly', () => {
2226
const events: Props['events'] = [
@@ -66,7 +70,7 @@ describe('WorkflowHistoryEventsCard', () => {
6670
).not.toBeInTheDocument();
6771
});
6872

69-
it('render accordion expanded when get getIsEventExpanded returns faltruese', async () => {
73+
it('render accordion expanded when get getIsEventExpanded returns true', async () => {
7074
const events: Props['events'] = [scheduleActivityTaskEvent];
7175
const eventsMetadata: Props['eventsMetadata'] = [
7276
{
@@ -102,17 +106,69 @@ describe('WorkflowHistoryEventsCard', () => {
102106
status: 'ONGOING',
103107
},
104108
];
109+
105110
const { user, mockedOnEventToggle } = setup({
106111
events,
107112
eventsMetadata,
108113
});
114+
109115
expect(
110116
screen.queryByText(JSON.stringify(events[1]))
111117
).not.toBeInTheDocument();
112118

113119
await user.click(screen.getByText('Second event'));
114120

115-
expect(mockedOnEventToggle).toHaveBeenCalled();
121+
expect(mockedOnEventToggle).toHaveBeenCalledWith('9');
122+
});
123+
124+
it('should show copy event button when the accordion is expanded', async () => {
125+
// TODO: this is a bit hacky, see if there is a better way to mock window properties
126+
const originalWindow = window;
127+
window = Object.create(window);
128+
Object.defineProperty(window, 'location', {
129+
value: {
130+
...window.location,
131+
origin: 'http://localhost',
132+
pathname: '/domains/mock-domain/workflows/wfid/runid/history',
133+
},
134+
writable: true,
135+
});
136+
137+
const events: Props['events'] = [
138+
scheduleActivityTaskEvent,
139+
startActivityTaskEvent,
140+
];
141+
142+
const eventsMetadata: Props['eventsMetadata'] = [
143+
{
144+
label: 'First event',
145+
status: 'COMPLETED',
146+
},
147+
{
148+
label: 'Second event',
149+
status: 'ONGOING',
150+
},
151+
];
152+
153+
const { user } = setup({
154+
events,
155+
eventsMetadata,
156+
getIsEventExpanded: jest.fn().mockReturnValue(true),
157+
});
158+
159+
const shareButtons = await screen.findAllByTestId('share-button');
160+
expect(shareButtons).toHaveLength(2);
161+
162+
await user.hover(shareButtons[0]);
163+
expect(await screen.findByText('Copy link to event')).toBeInTheDocument();
164+
165+
await user.click(shareButtons[0]);
166+
expect(copy).toHaveBeenCalledWith(
167+
'http://localhost/domains/mock-domain/workflows/wfid/runid/history?he=7'
168+
);
169+
expect(await screen.findByText('Copied link to event')).toBeInTheDocument();
170+
171+
window = originalWindow;
116172
});
117173

118174
it('should add placeholder event when showMissingEventPlaceholder is set to true', async () => {

src/views/workflow-history/workflow-history-events-card/workflow-history-events-card.styles.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import { type Theme, withStyle } from 'baseui';
2-
import {
3-
type StatelessAccordion,
4-
type AccordionOverrides,
5-
} from 'baseui/accordion';
1+
import { type Theme } from 'baseui';
2+
import { type AccordionOverrides } from 'baseui/accordion';
3+
import { type ButtonOverrides } from 'baseui/button';
64
import { type SkeletonOverrides } from 'baseui/skeleton/types';
7-
import { StyledTableHeadCell } from 'baseui/table-semantic';
85
import { type StyleObject } from 'styletron-react';
96

107
import type {
@@ -81,20 +78,34 @@ export const overrides = (animateBorderOnEnter?: boolean) => ({
8178
}),
8279
},
8380
} satisfies AccordionOverrides,
81+
shareButton: {
82+
BaseButton: {
83+
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
84+
height: $theme.sizing.scale600,
85+
width: $theme.sizing.scale600,
86+
}),
87+
},
88+
} satisfies ButtonOverrides,
8489
});
8590

8691
const cssStylesObj = {
87-
eventLabel: ($theme: Theme) => ({
88-
...$theme.typography.LabelSmall,
89-
color: $theme.colors.contentPrimary,
90-
flex: 1,
92+
eventLabel: (theme: Theme) => ({
93+
...theme.typography.LabelSmall,
94+
color: theme.colors.contentPrimary,
95+
display: 'flex',
96+
gap: theme.sizing.scale400,
97+
alignItems: 'center',
9198
}),
9299
skeletonContainer: ($theme: Theme) => ({
93100
display: 'flex',
94101
alignItems: 'center',
95102
gap: $theme.sizing.scale500,
96103
}),
97-
104+
eventPanelTitle: (theme) => ({
105+
display: 'flex',
106+
gap: theme.sizing.scale500,
107+
alignItems: 'center',
108+
}),
98109
detailsRow: (theme) => ({
99110
display: 'flex',
100111
flexDirection: 'row',

src/views/workflow-history/workflow-history-events-card/workflow-history-events-card.tsx

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
'use client';
2-
import React from 'react';
2+
import React, { useState } from 'react';
33

44
import { StatelessAccordion, Panel } from 'baseui/accordion';
5+
import { Button } from 'baseui/button';
56
import { Skeleton } from 'baseui/skeleton';
7+
import { StatefulTooltip } from 'baseui/tooltip';
8+
import copy from 'copy-to-clipboard';
9+
import queryString from 'query-string';
10+
import { MdLink } from 'react-icons/md';
611

712
import useStyletronClasses from '@/hooks/use-styletron-classes';
813

@@ -27,6 +32,8 @@ export default function WorkflowHistoryEventsCard({
2732
}: Props) {
2833
const { cls, theme } = useStyletronClasses(cssStyles);
2934

35+
const [isEventLinkCopied, setIsEventLinkCopied] = useState(false);
36+
3037
if (!eventsMetadata?.length && !showEventPlaceholder) return null;
3138
const expanded = events.reduce((result, event) => {
3239
const id = event.eventId === null ? event.computedEventId : event.eventId;
@@ -41,18 +48,62 @@ export default function WorkflowHistoryEventsCard({
4148
const event = events[index];
4249
const id =
4350
event.eventId === null ? event.computedEventId : event.eventId;
51+
const isPanelExpanded = expanded.includes(id);
52+
4453
return (
4554
<Panel
4655
key={id}
4756
title={
48-
<>
57+
<div className={cls.eventPanelTitle}>
4958
<WorkflowHistoryEventStatusBadge
5059
statusReady={true}
5160
size="small"
5261
status={eventMetadata.status}
5362
/>
54-
<div className={cls.eventLabel}>{eventMetadata.label}</div>
55-
</>
63+
<div className={cls.eventLabel}>
64+
{eventMetadata.label}
65+
{isPanelExpanded && (
66+
<StatefulTooltip
67+
showArrow
68+
placement="right"
69+
popoverMargin={8}
70+
accessibilityType="tooltip"
71+
content={() =>
72+
isEventLinkCopied
73+
? 'Copied link to event'
74+
: 'Copy link to event'
75+
}
76+
onMouseLeave={() => setIsEventLinkCopied(false)}
77+
returnFocus
78+
autoFocus
79+
>
80+
<Button
81+
data-testid="share-button"
82+
size="mini"
83+
shape="circle"
84+
kind="tertiary"
85+
overrides={overrides.shareButton}
86+
onClick={(e) => {
87+
e.stopPropagation();
88+
copy(
89+
queryString.stringifyUrl({
90+
url:
91+
window.location.origin +
92+
window.location.pathname,
93+
query: {
94+
he: id,
95+
},
96+
})
97+
);
98+
setIsEventLinkCopied(true);
99+
}}
100+
>
101+
<MdLink />
102+
</Button>
103+
</StatefulTooltip>
104+
)}
105+
</div>
106+
</div>
56107
}
57108
onClick={() => onEventToggle(id)}
58109
>

src/views/workflow-history/workflow-history.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,13 @@ export default function WorkflowHistory({ params }: Props) {
230230
getIsEventExpanded,
231231
} = useEventExpansionToggle({
232232
visibleEvents: filteredEvents,
233+
...(queryParams.historySelectedEventId
234+
? {
235+
initialState: {
236+
[queryParams.historySelectedEventId]: true,
237+
},
238+
}
239+
: {}),
233240
});
234241

235242
const [isTimelineChartShown, setIsTimelineChartShown] = useState(false);
@@ -355,9 +362,9 @@ export default function WorkflowHistory({ params }: Props) {
355362
secondaryLabel={timeLabel}
356363
showLabelPlaceholder={!label}
357364
badges={badges}
358-
selected={
359-
queryParams.historySelectedEventId === events[0].eventId
360-
}
365+
selected={events.some(
366+
(e) => e.eventId === queryParams.historySelectedEventId
367+
)}
361368
disabled={!Boolean(events[0].eventId)}
362369
onClick={() => {
363370
if (events[0].eventId)
@@ -422,10 +429,9 @@ export default function WorkflowHistory({ params }: Props) {
422429
setResetToDecisionEventId(group.resetToDecisionEventId);
423430
}
424431
}}
425-
selected={
426-
queryParams.historySelectedEventId ===
427-
group.events[0].eventId
428-
}
432+
selected={group.events.some(
433+
(e) => e.eventId === queryParams.historySelectedEventId
434+
)}
429435
/>
430436
)}
431437
components={{

0 commit comments

Comments
 (0)