Skip to content

Commit 422ea82

Browse files
authored
feat(webapp): add modal for custom export name (#965)
* feat(webapp): add modal for custom export name
1 parent 05ac4e4 commit 422ea82

File tree

8 files changed

+185
-38
lines changed

8 files changed

+185
-38
lines changed

webapp/javascript/components/ExportData.tsx

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { faBars } from '@fortawesome/free-solid-svg-icons/faBars';
66
import { buildRenderURL } from '@webapp/util/updateRequests';
77
import { dateForExportFilename } from '@webapp/util/formatDate';
88
import { Profile } from '@pyroscope/models';
9+
import showModalWithInput from './Modals/ModalWithInput';
910

1011
import styles from './ExportData.module.scss';
1112

@@ -37,7 +38,7 @@ type exportHTML =
3738
type exportFlamegraphDotCom =
3839
| {
3940
exportFlamegraphDotCom: true;
40-
exportFlamegraphDotComFn: () => Promise<string | null>;
41+
exportFlamegraphDotComFn: (name?: string) => Promise<string | null>;
4142
flamebearer: Profile;
4243
}
4344
| { exportFlamegraphDotCom?: false };
@@ -75,19 +76,26 @@ function ExportData(props: ExportDataProps) {
7576

7677
const [toggleMenu, setToggleMenu] = useState(false);
7778

78-
const downloadJSON = () => {
79+
const downloadJSON = async () => {
7980
if (!props.exportJSON) {
8081
return;
8182
}
8283

8384
// TODO additional check this won't be needed once we use strictNullChecks
8485
if (props.exportJSON) {
8586
const { flamebearer } = props;
86-
const filename = `${getFilename(
87+
88+
const defaultExportName = getFilename(
8789
flamebearer.metadata.appName,
8890
flamebearer.metadata.startTime,
8991
flamebearer.metadata.endTime
90-
)}.json`;
92+
);
93+
// get user input from modal
94+
const customExportName = await getCustomExportName(defaultExportName);
95+
// return if user cancels the modal
96+
if (!customExportName) return;
97+
98+
const filename = `${customExportName}.json`;
9199

92100
const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(
93101
JSON.stringify(flamebearer)
@@ -101,14 +109,26 @@ function ExportData(props: ExportDataProps) {
101109
}
102110
};
103111

104-
const downloadFlamegraphDotCom = () => {
112+
const downloadFlamegraphDotCom = async () => {
105113
if (!props.exportFlamegraphDotCom) {
106114
return;
107115
}
108116

109117
// TODO additional check this won't be needed once we use strictNullChecks
110118
if (props.exportFlamegraphDotCom) {
111-
props.exportFlamegraphDotComFn().then((url) => {
119+
const { flamebearer } = props;
120+
121+
const defaultExportName = getFilename(
122+
flamebearer.metadata.appName,
123+
flamebearer.metadata.startTime,
124+
flamebearer.metadata.endTime
125+
);
126+
// get user input from modal
127+
const customExportName = await getCustomExportName(defaultExportName);
128+
// return if user cancels the modal
129+
if (!customExportName) return;
130+
131+
props.exportFlamegraphDotComFn(customExportName).then((url) => {
112132
// there has been an error which should've been handled
113133
// so we just ignore it
114134
if (!url) {
@@ -126,9 +146,22 @@ function ExportData(props: ExportDataProps) {
126146
}
127147
};
128148

129-
const downloadPNG = () => {
149+
const downloadPNG = async () => {
130150
if (props.exportPNG) {
131151
const { flamebearer } = props;
152+
153+
const defaultExportName = getFilename(
154+
flamebearer.metadata.appName,
155+
flamebearer.metadata.startTime,
156+
flamebearer.metadata.endTime
157+
);
158+
// get user input from modal
159+
const customExportName = await getCustomExportName(defaultExportName);
160+
// return if user cancels the modal
161+
if (!customExportName) return;
162+
163+
const filename = `${customExportName}.png`;
164+
132165
const mimeType = 'png';
133166
// TODO use ref
134167
// this won't work for comparison side by side
@@ -138,11 +171,6 @@ function ExportData(props: ExportDataProps) {
138171
const MIME_TYPE = `image/${mimeType}`;
139172
const imgURL = canvasElement.toDataURL();
140173
const dlLink = document.createElement('a');
141-
const filename = `${getFilename(
142-
flamebearer.metadata.appName,
143-
flamebearer.metadata.startTime,
144-
flamebearer.metadata.endTime
145-
)}.png`;
146174

147175
dlLink.download = filename;
148176
dlLink.href = imgURL;
@@ -202,7 +230,7 @@ function ExportData(props: ExportDataProps) {
202230
}
203231
};
204232

205-
const downloadHTML = function () {
233+
const downloadHTML = async function () {
206234
if (props.exportHTML) {
207235
const { flamebearer } = props;
208236

@@ -227,11 +255,18 @@ function ExportData(props: ExportDataProps) {
227255
maxNodes: flamebearer.metadata.maxNodes,
228256
});
229257
const urlWithFormat = `${url}&format=html`;
230-
const filename = `${getFilename(
258+
259+
const defaultExportName = getFilename(
231260
flamebearer.metadata.appName,
232261
flamebearer.metadata.startTime,
233262
flamebearer.metadata.endTime
234-
)}.html`;
263+
);
264+
// get user input from modal
265+
const customExportName = await getCustomExportName(defaultExportName);
266+
// return if user cancels the modal
267+
if (!customExportName) return;
268+
269+
const filename = `${customExportName}.html`;
235270

236271
const downloadAnchorNode = document.createElement('a');
237272
downloadAnchorNode.setAttribute('href', urlWithFormat);
@@ -242,15 +277,28 @@ function ExportData(props: ExportDataProps) {
242277
}
243278
};
244279

280+
async function getCustomExportName(defaultExportName: string) {
281+
return showModalWithInput({
282+
title: 'Enter export name',
283+
confirmButtonText: 'Export',
284+
input: 'text',
285+
inputValue: defaultExportName,
286+
inputPlaceholder: 'Export name',
287+
type: 'normal',
288+
validationMessage: 'Name must not be empty',
289+
onConfirm: (value: any) => value,
290+
});
291+
}
292+
245293
return (
246294
<div className={styles.dropdownContainer}>
247295
<Button icon={faBars} onClick={handleToggleMenu} />
248296
<div className={toggleMenu ? styles.menuShow : styles.menuHide}>
249297
{exportPNG && (
250298
<button
251299
className={styles.dropdownMenuItem}
252-
onClick={() => downloadPNG()}
253-
onKeyPress={() => downloadPNG()}
300+
onClick={downloadPNG}
301+
onKeyPress={downloadPNG}
254302
type="button"
255303
>
256304
png
@@ -260,7 +308,7 @@ function ExportData(props: ExportDataProps) {
260308
<button
261309
className={styles.dropdownMenuItem}
262310
type="button"
263-
onClick={() => downloadJSON()}
311+
onClick={downloadJSON}
264312
>
265313
json
266314
</button>
@@ -269,7 +317,7 @@ function ExportData(props: ExportDataProps) {
269317
<button
270318
className={styles.dropdownMenuItem}
271319
type="button"
272-
onClick={() => downloadPprof()}
320+
onClick={downloadPprof}
273321
>
274322
pprof
275323
</button>
@@ -278,7 +326,7 @@ function ExportData(props: ExportDataProps) {
278326
<button
279327
className={styles.dropdownMenuItem}
280328
type="button"
281-
onClick={() => downloadHTML()}
329+
onClick={downloadHTML}
282330
>
283331
{' '}
284332
html
@@ -288,7 +336,7 @@ function ExportData(props: ExportDataProps) {
288336
<button
289337
className={styles.dropdownMenuItem}
290338
type="button"
291-
onClick={() => downloadFlamegraphDotCom()}
339+
onClick={downloadFlamegraphDotCom}
292340
>
293341
{' '}
294342
flamegraph.com

webapp/javascript/components/ConfirmDelete/index.tsx renamed to webapp/javascript/components/Modals/ConfirmDelete/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ function confirmDelete(object: string, onConfirm: () => void) {
44
ShowModal({
55
title: `Are you sure you want to delete ${object}?`,
66
confirmButtonText: 'Delete',
7-
danger: true,
7+
type: 'danger',
88
onConfirm,
99
});
1010
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import ShowModal, { ShowModalParams } from '@webapp/ui/Modals';
2+
3+
type ModalWithInputParams = Pick<
4+
ShowModalParams,
5+
| 'title'
6+
| 'input'
7+
| 'inputPlaceholder'
8+
| 'confirmButtonText'
9+
| 'onConfirm'
10+
| 'inputValue'
11+
| 'type'
12+
| 'validationMessage'
13+
>;
14+
15+
async function showModalWithInput(params: ModalWithInputParams) {
16+
return ShowModal({
17+
...params,
18+
});
19+
}
20+
21+
export default showModalWithInput;

webapp/javascript/components/Settings/APIKeys/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
selectAPIKeys,
1414
deleteAPIKey,
1515
} from '@webapp/redux/reducers/settings';
16-
import confirmDelete from '@webapp/components/ConfirmDelete';
16+
import confirmDelete from '@webapp/components/Modals/ConfirmDelete';
1717
import styles from '../SettingsTable.module.css';
1818

1919
const ApiKeys = () => {

webapp/javascript/components/Settings/Users/UserTableItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import cx from 'classnames';
1111
import Dropdown, { MenuItem } from '@webapp/ui/Dropdown';
1212
import { reloadUsers, changeUserRole } from '@webapp/redux/reducers/settings';
1313
import { useAppDispatch } from '@webapp/redux/hooks';
14-
import confirmDelete from '@webapp/components/ConfirmDelete';
14+
import confirmDelete from '@webapp/components/Modals/ConfirmDelete';
1515
import { type User } from '@webapp/models/users';
1616
import styles from './UserTableItem.module.css';
1717

webapp/javascript/components/exportToFlamegraphDotCom.hook.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ import handleError from '@webapp/util/handleError';
66
export default function useExportToFlamegraphDotCom(flamebearer?: Profile) {
77
const dispatch = useAppDispatch();
88

9-
return async () => {
9+
return async (name?: string) => {
1010
if (!flamebearer) {
1111
return '';
1212
}
1313

14-
const res = await shareWithFlamegraphDotcom({ flamebearer });
14+
const res = await shareWithFlamegraphDotcom({
15+
flamebearer,
16+
name,
17+
});
1518

1619
if (res.isErr) {
1720
handleError(dispatch, 'Failed to export to flamegraph.com', res.error);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
.popup {
2+
background-color: var(--ps-c-gray);
3+
}
4+
5+
.popup label {
6+
color: var(--ps-c-white);
7+
}
8+
9+
.popup button.button:focus {
10+
box-shadow: none;
11+
}
12+
13+
.title {
14+
color: var(--ps-c-white);
15+
}
16+
17+
.input {
18+
outline: none;
19+
color: var(--ps-c-white);
20+
background-color: var(--ps-c-gray-light-2);
21+
border: 1px solid var(--ps-c-gray-light-3);
22+
border-radius: 4px;
23+
}
24+
25+
.input:focus {
26+
outline: none;
27+
box-shadow: none;
28+
border: 1px solid var(--ps-c-gray-light-3);
29+
}
30+
31+
.validationMessage {
32+
color: var(--ps-c-white);
33+
font-weight: bold;
34+
background-color: var(--ps-c-red-error);
35+
}

webapp/javascript/ui/Modals/index.ts

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,74 @@
1-
import Swal, { SweetAlertOptions } from 'sweetalert2';
1+
import Swal, { SweetAlertInput, SweetAlertOptions } from 'sweetalert2';
2+
3+
import styles from './Modal.module.css';
24

35
const defaultParams: Partial<SweetAlertOptions> = {
46
showCancelButton: true,
57
allowOutsideClick: true,
68
backdrop: true,
9+
focusConfirm: false,
10+
customClass: {
11+
popup: styles.popup,
12+
title: styles.title,
13+
input: styles.input,
14+
confirmButton: styles.button,
15+
denyButton: styles.button,
16+
cancelButton: styles.button,
17+
validationMessage: styles.validationMessage,
18+
},
19+
inputAttributes: {
20+
required: 'true',
21+
},
722
};
823

9-
type ShowModalParams = {
24+
export type ShowModalParams = {
1025
title: string;
1126
confirmButtonText: string;
12-
danger: boolean;
13-
onConfirm: ShamefulAny;
27+
type: 'danger' | 'normal';
28+
onConfirm?: ShamefulAny;
29+
input?: SweetAlertInput;
30+
inputValue?: string;
31+
inputLabel?: string;
32+
inputPlaceholder?: string;
33+
validationMessage?: string;
1434
};
1535

16-
const ShowModal = ({
36+
const ShowModal = async ({
1737
title,
1838
confirmButtonText,
19-
danger,
39+
type,
2040
onConfirm,
41+
input,
42+
inputValue,
43+
inputLabel,
44+
inputPlaceholder,
45+
validationMessage,
2146
}: ShowModalParams) => {
22-
Swal.fire({
47+
const { isConfirmed, value } = await Swal.fire({
2348
title,
2449
confirmButtonText,
25-
confirmButtonColor: danger ? '#dc3545' : '#0074d9',
50+
input,
51+
inputLabel,
52+
inputPlaceholder,
53+
inputValue,
54+
validationMessage,
55+
confirmButtonColor: getButtonStyleFromType(type),
2656
...defaultParams,
27-
}).then((result) => {
28-
if (result.isConfirmed) {
29-
onConfirm();
30-
}
3157
});
58+
59+
if (isConfirmed) {
60+
onConfirm(value);
61+
}
62+
63+
return value;
3264
};
3365

66+
function getButtonStyleFromType(type: 'danger' | 'normal') {
67+
if (type === 'danger') {
68+
return '#dc3545'; // red
69+
}
70+
71+
return '#0074d9'; // blue
72+
}
73+
3474
export default ShowModal;

0 commit comments

Comments
 (0)