Skip to content

Commit a96c486

Browse files
committed
fix: execution plan svg not saving in chrome
1 parent bc8acee commit a96c486

File tree

3 files changed

+96
-18
lines changed

3 files changed

+96
-18
lines changed

src/containers/Tenant/Query/QueryResult/components/PlanToSvgButton/PlanToSvgButton.tsx

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import React from 'react';
22

3-
import {ArrowUpRightFromSquare} from '@gravity-ui/icons';
4-
import {Button, Tooltip} from '@gravity-ui/uikit';
3+
import {ArrowDownToLine, ArrowUpRightFromSquare, ChevronDown} from '@gravity-ui/icons';
4+
import type {ButtonProps} from '@gravity-ui/uikit';
5+
import {Button, DropdownMenu} from '@gravity-ui/uikit';
56

67
import {planToSvgApi} from '../../../../../../store/reducers/planToSvg';
78
import type {QueryPlan, ScriptPlan} from '../../../../../../types/api/query';
@@ -24,20 +25,42 @@ export function PlanToSvgButton({plan, database}: PlanToSvgButtonProps) {
2425
const [blobUrl, setBlobUrl] = React.useState<string | null>(null);
2526
const [getPlanToSvg, {isLoading}] = planToSvgApi.useLazyPlanToSvgQueryQuery();
2627

27-
const handleClick = React.useCallback(() => {
28-
getPlanToSvg({plan, database})
28+
const handleGetSvg = React.useCallback(() => {
29+
if (blobUrl) {
30+
return Promise.resolve(blobUrl);
31+
}
32+
33+
return getPlanToSvg({plan, database})
2934
.unwrap()
3035
.then((result) => {
3136
const blob = new Blob([result], {type: 'image/svg+xml'});
3237
const url = URL.createObjectURL(blob);
3338
setBlobUrl(url);
3439
setError(null);
35-
window.open(url, '_blank');
40+
return url;
3641
})
3742
.catch((err) => {
3843
setError(JSON.stringify(err));
44+
throw err;
3945
});
40-
}, [database, getPlanToSvg, plan]);
46+
}, [database, getPlanToSvg, plan, blobUrl]);
47+
48+
const handleOpenInNewTab = React.useCallback(() => {
49+
handleGetSvg().then((url) => {
50+
window.open(url, '_blank');
51+
});
52+
}, [handleGetSvg]);
53+
54+
const handleDownload = React.useCallback(() => {
55+
handleGetSvg().then((url) => {
56+
const link = document.createElement('a');
57+
link.href = url;
58+
link.download = 'query-plan.svg';
59+
document.body.appendChild(link);
60+
link.click();
61+
document.body.removeChild(link);
62+
});
63+
}, [handleGetSvg]);
4164

4265
React.useEffect(() => {
4366
return () => {
@@ -47,21 +70,34 @@ export function PlanToSvgButton({plan, database}: PlanToSvgButtonProps) {
4770
};
4871
}, [blobUrl]);
4972

50-
return (
51-
<Tooltip
52-
content={error ? i18n('text_error-plan-svg', {error}) : i18n('text_open-plan-svg')}
53-
>
73+
const items = [
74+
{
75+
text: i18n('text_open-new-tab'),
76+
icon: <ArrowUpRightFromSquare />,
77+
action: handleOpenInNewTab,
78+
},
79+
{
80+
text: i18n('text_download'),
81+
icon: <ArrowDownToLine />,
82+
action: handleDownload,
83+
},
84+
];
85+
86+
const renderSwitcher = (props: ButtonProps) => {
87+
return (
5488
<Button
5589
view={getButtonView(error, isLoading)}
5690
loading={isLoading}
57-
onClick={handleClick}
5891
disabled={isLoading}
92+
{...props}
5993
>
6094
{i18n('text_plan-svg')}
6195
<Button.Icon>
62-
<ArrowUpRightFromSquare />
96+
<ChevronDown />
6397
</Button.Icon>
6498
</Button>
65-
</Tooltip>
66-
);
99+
);
100+
};
101+
102+
return <DropdownMenu renderSwitcher={renderSwitcher} items={items} />;
67103
}

src/containers/Tenant/Query/QueryResult/i18n/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"title.truncated": "Truncated",
1313
"title.result": "Result",
1414
"text_plan-svg": "Execution plan",
15-
"text_open-plan-svg": "Open execution plan in new window",
15+
"text_open-new-tab": "Open in new tab",
16+
"text_download": "Download",
1617
"text_error-plan-svg": "Error: {{error}}"
1718
}

tests/suites/tenant/queryEditor/planToSvg.test.ts

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ test.describe('Test Plan to SVG functionality', async () => {
2121
await tenantPage.goto(pageQueryParams);
2222
});
2323

24-
test('Plan to SVG experiment shows execution plan in new tab', async ({page}) => {
24+
test('Plan to SVG dropdown shows options and opens plan in new tab', async ({page}) => {
2525
const queryEditor = new QueryEditor(page);
2626

2727
// 1. Turn on Plan to SVG experiment
@@ -37,17 +37,58 @@ test.describe('Test Plan to SVG functionality', async () => {
3737
expect(status).toBe('Completed');
3838
}).toPass();
3939

40-
// 4. Check if Execution Plan button appears and click it
40+
// 4. Check if Execution Plan button appears and click it to open dropdown
4141
const executionPlanButton = page.locator('button:has-text("Execution plan")');
4242
await expect(executionPlanButton).toBeVisible();
4343
await executionPlanButton.click();
44+
45+
// 5. Verify dropdown menu items are visible
46+
const openInNewTabOption = page.locator('text="Open in new tab"');
47+
const downloadOption = page.locator('text="Download"');
48+
await expect(openInNewTabOption).toBeVisible();
49+
await expect(downloadOption).toBeVisible();
50+
51+
// 6. Click "Open in new tab" option
52+
await openInNewTabOption.click();
4453
await page.waitForTimeout(1000); // Wait for new tab to open
4554

46-
// 5. Verify we're taken to a new tab with SVG content
55+
// 7. Verify we're taken to a new tab with SVG content
4756
const svgElement = page.locator('svg').first();
4857
await expect(svgElement).toBeVisible();
4958
});
5059

60+
test('Plan to SVG download option triggers file download', async ({page}) => {
61+
const queryEditor = new QueryEditor(page);
62+
63+
// 1. Turn on Plan to SVG experiment
64+
await toggleExperiment(page, 'on', 'Execution plan');
65+
66+
// 2. Set query and run it
67+
await queryEditor.setQuery(testQuery);
68+
await queryEditor.clickRunButton();
69+
70+
// 3. Wait for query execution to complete
71+
await expect(async () => {
72+
const status = await queryEditor.getExecutionStatus();
73+
expect(status).toBe('Completed');
74+
}).toPass();
75+
76+
// 4. Click execution plan button to open dropdown
77+
const executionPlanButton = page.locator('button:has-text("Execution plan")');
78+
await executionPlanButton.click();
79+
80+
// 5. Setup download listener before clicking download
81+
const downloadPromise = page.waitForEvent('download');
82+
83+
// 6. Click download option
84+
const downloadOption = page.locator('text="Download"');
85+
await downloadOption.click();
86+
87+
// 7. Wait for download to start and verify filename
88+
const download = await downloadPromise;
89+
expect(download.suggestedFilename()).toBe('query-plan.svg');
90+
});
91+
5192
test('Statistics setting becomes disabled when execution plan experiment is enabled', async ({
5293
page,
5394
}) => {

0 commit comments

Comments
 (0)