Skip to content

Commit e5caab4

Browse files
authored
Fix: update sharelink url format for e2 (#58)
* initial approach * include only runs when both asset and run is specified
1 parent 93b1592 commit e5caab4

6 files changed

Lines changed: 224 additions & 406 deletions

File tree

src/components/VisualSiftQueryEditor.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ describe('VisualSiftQueryEditor', () => {
8080
expect(openInSiftButtonProps).toMatchObject({
8181
apiBaseUrl: 'https://sift.example.com',
8282
frontendUrl: undefined,
83+
queryType: QueryTypes.CHANNEL,
8384
});
8485
});
8586

@@ -110,6 +111,11 @@ describe('VisualSiftQueryEditor', () => {
110111
const radioButton = screen.getByLabelText('Calculated Channels');
111112
expect(radioButton).toBeChecked();
112113
});
114+
115+
const openInSiftButtonProps = (OpenInSiftButton as jest.Mock).mock.calls.at(-1)?.[0];
116+
expect(openInSiftButtonProps).toMatchObject({
117+
queryType: QueryTypes.CALCULATED_CHANNEL,
118+
});
113119
});
114120

115121
it('sets queryMode to CHANNEL if migrated query has no calculated channels', async () => {
@@ -467,6 +473,7 @@ describe('VisualSiftQueryEditor', () => {
467473
from: fromDate.toISOString(),
468474
to: toDate.toISOString(),
469475
},
476+
queryType: QueryTypes.CHANNEL,
470477
}),
471478
expect.anything()
472479
);
@@ -509,6 +516,7 @@ describe('VisualSiftQueryEditor', () => {
509516
from: fromIsoString,
510517
to: toIsoString,
511518
},
519+
queryType: QueryTypes.CHANNEL,
512520
}),
513521
expect.anything()
514522
);
@@ -540,6 +548,7 @@ describe('VisualSiftQueryEditor', () => {
540548
expect(OpenInSiftButton).toHaveBeenCalledWith(
541549
expect.objectContaining({
542550
timeRange: undefined,
551+
queryType: QueryTypes.CHANNEL,
543552
}),
544553
expect.anything()
545554
);

src/components/VisualSiftQueryEditor.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ export const VisualSiftQueryEditor = (props: Props) => {
164164
apiBaseUrl={apiRestUrl}
165165
frontendUrl={frontendUrl}
166166
timeRange={shareLinkTimeRange}
167+
queryType={queryMode}
167168
/>
168169

169170
</InlineFieldRow>

src/components/sharelink/OpenInSiftButton.test.tsx

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { OpenInSiftButton } from './OpenInSiftButton';
44
import { generateLinkFromQuery } from './generateLinkFromQuery';
55
import { getFrontendHostnameDefaults } from './getFrontendHostnameDefaults';
66
import { getAppEvents } from '@grafana/runtime';
7+
import { QueryTypes } from '../../types';
78

89
jest.mock('./generateLinkFromQuery', () => ({
910
generateLinkFromQuery: jest.fn(),
@@ -26,12 +27,14 @@ const getAppEventsMock = getAppEvents as jest.MockedFunction<typeof getAppEvents
2627
describe('OpenInSiftButton', () => {
2728
let logSpy: jest.SpyInstance;
2829
let errorSpy: jest.SpyInstance;
30+
let publishMock: jest.Mock;
2931

3032
beforeEach(() => {
3133
jest.clearAllMocks();
32-
generateLinkFromQueryMock.mockReturnValue('https://sift.example.com/explorer');
34+
generateLinkFromQueryMock.mockReturnValue('https://sift.example.com/explore?method=single');
3335
getFrontendHostnameDefaultsMock.mockReturnValue('sift.example.com');
34-
getAppEventsMock.mockReturnValue(null as any);
36+
publishMock = jest.fn();
37+
getAppEventsMock.mockReturnValue({ publish: publishMock } as any);
3538
logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
3639
errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
3740
});
@@ -64,7 +67,7 @@ describe('OpenInSiftButton', () => {
6467
const button = screen.getByRole('button', { name: 'Open in Sift' });
6568
fireEvent.click(button);
6669

67-
expect(openSpy).toHaveBeenCalledWith('https://sift.example.com/explorer', '_blank', 'noopener,noreferrer');
70+
expect(openSpy).toHaveBeenCalledWith('https://sift.example.com/explore?method=single', '_blank', 'noopener,noreferrer');
6871
openSpy.mockRestore();
6972
});
7073

@@ -96,7 +99,7 @@ describe('OpenInSiftButton', () => {
9699
const button = screen.getByRole('button', { name: 'Open in Sift' });
97100
fireEvent.click(button);
98101

99-
expect(openSpy).toHaveBeenCalledWith('https://sift.example.com/explorer', '_blank', 'noopener,noreferrer');
102+
expect(openSpy).toHaveBeenCalledWith('https://sift.example.com/explore?method=single', '_blank', 'noopener,noreferrer');
100103
openSpy.mockRestore();
101104
});
102105

@@ -148,7 +151,7 @@ describe('OpenInSiftButton', () => {
148151
const button = screen.getByRole('button', { name: 'Open in Sift' });
149152
fireEvent.click(button);
150153

151-
expect(openSpy).toHaveBeenCalledWith('https://sift.example.com/explorer', '_blank', 'noopener,noreferrer');
154+
expect(openSpy).toHaveBeenCalledWith('https://sift.example.com/explore?method=single', '_blank', 'noopener,noreferrer');
152155
openSpy.mockRestore();
153156
});
154157

@@ -262,4 +265,39 @@ describe('OpenInSiftButton', () => {
262265
expect(screen.getByRole('menuitem', { name: /Open Link/i })).toBeDisabled();
263266
});
264267
});
268+
269+
it('disables share link when Query Mode is set to Calculated Channels', async () => {
270+
const items = {
271+
channelIds: ['channel-1'],
272+
assetIds: ['asset-1'],
273+
runIds: ['run-1'],
274+
calculatedChannels: [
275+
{
276+
name: 'calc-1',
277+
sourceChannels: ['channel-1'],
278+
expression: '$1',
279+
expressionDataType: 'double',
280+
},
281+
],
282+
};
283+
284+
render(
285+
<OpenInSiftButton
286+
items={items}
287+
apiBaseUrl="https://api.sift.dev"
288+
queryType={QueryTypes.CALCULATED_CHANNEL}
289+
/>
290+
);
291+
292+
expect(generateLinkFromQueryMock).not.toHaveBeenCalled();
293+
294+
const menuButton = screen.getByRole('button', { name: 'Open in Sift' });
295+
expect(menuButton).toHaveAttribute('aria-disabled', 'true');
296+
297+
fireEvent.contextMenu(menuButton);
298+
299+
await waitFor(() => {
300+
expect(screen.getByRole('menuitem', { name: /Open Link/i })).toBeDisabled();
301+
});
302+
});
265303
});

src/components/sharelink/OpenInSiftButton.tsx

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { AppEvents } from '@grafana/data';
44
import { getAppEvents } from '@grafana/runtime';
55
import { getFrontendHostnameDefaults } from './getFrontendHostnameDefaults';
66
import { generateLinkFromQuery } from './generateLinkFromQuery';
7-
import type { SharelinkItems, SharelinkTimeRange } from '../../types';
7+
import { QueryTypes, type QueryType, type SharelinkItems, type SharelinkTimeRange } from '../../types';
88
import { SquareShareIcon } from '../common/CustomIcons';
99

1010
interface SharelinkMenuItemProps {
@@ -13,43 +13,57 @@ interface SharelinkMenuItemProps {
1313
apiBaseUrl?: string;
1414
frontendUrl?: string;
1515
timeRange?: SharelinkTimeRange;
16+
queryType?: QueryType;
1617
}
1718

1819
function openLink(link: string) {
1920
window.open(link, '_blank', 'noopener,noreferrer');
2021
}
2122

22-
async function copyToClipboard(value: string) {
23-
try {
24-
if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') {
25-
throw new Error('Clipboard API not available');
26-
}
23+
function publishAlert(type: string, message: string) {
24+
const appEvents = getAppEvents();
25+
if (appEvents) {
26+
appEvents.publish({
27+
type,
28+
payload: [message],
29+
});
30+
}
31+
}
2732

28-
await navigator.clipboard.writeText(value);
29-
const appEvents = getAppEvents();
30-
if (appEvents) {
31-
appEvents.publish({
32-
type: AppEvents.alertSuccess.name,
33-
payload: ['Copied Sift share link to clipboard'],
34-
});
35-
}
36-
} catch (err) {
37-
console.error('Failed to copy link', err);
33+
async function copyToClipboard(value: string) {
34+
if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') {
35+
throw new Error('Clipboard API not available');
3836
}
37+
38+
await navigator.clipboard.writeText(value);
3939
}
4040

41-
export const OpenInSiftButton = ({ className, items, apiBaseUrl, frontendUrl, timeRange }: SharelinkMenuItemProps) => {
41+
export const OpenInSiftButton = ({
42+
className,
43+
items,
44+
apiBaseUrl,
45+
frontendUrl,
46+
timeRange,
47+
queryType,
48+
}: SharelinkMenuItemProps) => {
4249
const { shareLink, disabledReason } = useMemo(() => {
4350
const trimmedFrontendUrl = frontendUrl?.trim();
4451

52+
if (queryType === QueryTypes.CALCULATED_CHANNEL) {
53+
return {
54+
shareLink: null,
55+
disabledReason: 'Open in Sift is unavailable when Query Mode is set to Calculated Channels',
56+
};
57+
}
58+
4559
if (!apiBaseUrl && !trimmedFrontendUrl) {
4660
return {
4761
shareLink: null,
4862
disabledReason: 'Configure the Sift API REST URL to enable share links',
4963
};
5064
}
5165

52-
if (!items || !items.channelIds || items.channelIds.length === 0) {
66+
if (!items || items.channelIds.length === 0) {
5367
return {
5468
shareLink: null,
5569
disabledReason: 'Select a channel to enable share links',
@@ -63,7 +77,7 @@ export const OpenInSiftButton = ({ className, items, apiBaseUrl, frontendUrl, ti
6377
disabledReason: 'Configure the Sift API REST URL to enable share links',
6478
};
6579
}
66-
80+
6781
try {
6882
return {
6983
shareLink: generateLinkFromQuery(hostname, items, timeRange),
@@ -76,7 +90,29 @@ export const OpenInSiftButton = ({ className, items, apiBaseUrl, frontendUrl, ti
7690
disabledReason: 'Failed to generate share link',
7791
};
7892
}
79-
}, [apiBaseUrl, frontendUrl, items, timeRange]);
93+
}, [apiBaseUrl, frontendUrl, items, queryType, timeRange]);
94+
95+
const handleOpen = () => {
96+
if (!shareLink) {
97+
return;
98+
}
99+
100+
openLink(shareLink);
101+
};
102+
103+
const handleCopy = async () => {
104+
if (!shareLink) {
105+
return;
106+
}
107+
108+
try {
109+
await copyToClipboard(shareLink);
110+
111+
publishAlert(AppEvents.alertSuccess.name, 'Copied Sift share link to clipboard');
112+
} catch (err) {
113+
console.error('Failed to copy link', err);
114+
}
115+
};
80116

81117
return (
82118
<InlineLabel width="auto" transparent className={className} style={{ marginLeft: 'auto' }}>
@@ -87,18 +123,14 @@ export const OpenInSiftButton = ({ className, items, apiBaseUrl, frontendUrl, ti
87123
label="Open Link"
88124
disabled={!shareLink}
89125
onClick={() => {
90-
if (shareLink) {
91-
openLink(shareLink);
92-
}
126+
handleOpen();
93127
}}
94128
/>
95129
<Menu.Item
96130
label={shareLink ? 'Copy to Clipboard' : 'Copy (URL not set)'}
97131
disabled={!shareLink}
98132
onClick={() => {
99-
if (shareLink) {
100-
void copyToClipboard(shareLink);
101-
}
133+
void handleCopy();
102134
}}
103135
/>
104136
</Menu.Group>
@@ -108,7 +140,7 @@ export const OpenInSiftButton = ({ className, items, apiBaseUrl, frontendUrl, ti
108140
<Button
109141
onClick={(event) => {
110142
if (shareLink) {
111-
openLink(shareLink);
143+
handleOpen();
112144
return;
113145
}
114146
openMenu(event);

0 commit comments

Comments
 (0)