From 93d770861d2f85711ac256bc200dbac54a728816 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 15 Jul 2024 15:17:54 +0300 Subject: [PATCH] Allow submitting forms in the UI by pressing "Enter" (#4674) # What this PR does - Escalation Chain and Token forms now use `react-hook-form` ## Which issue(s) this PR closes Closes https://github.com/grafana/oncall/issues/2038 --- .../ManualAlertGroup/ManualAlertGroup.tsx | 19 +- .../ApiTokenSettings/ApiTokenForm.module.css | 6 +- .../ApiTokenSettings/ApiTokenForm.tsx | 119 +++++++---- .../EscalationChainForm.tsx | 193 +++++++++++------- 4 files changed, 211 insertions(+), 126 deletions(-) diff --git a/src/components/ManualAlertGroup/ManualAlertGroup.tsx b/src/components/ManualAlertGroup/ManualAlertGroup.tsx index b2f39a5e0d..6a9f2fa898 100644 --- a/src/components/ManualAlertGroup/ManualAlertGroup.tsx +++ b/src/components/ManualAlertGroup/ManualAlertGroup.tsx @@ -1,5 +1,6 @@ import React, { FC, useCallback } from 'react'; +import { css } from '@emotion/css'; import { Button, Drawer, Field, HorizontalGroup, TextArea, useStyles2, VerticalGroup } from '@grafana/ui'; import { observer } from 'mobx-react'; import { Controller, FormProvider, useForm } from 'react-hook-form'; @@ -50,7 +51,6 @@ export const ManualAlertGroup: FC = observer(({ onCreate, const onSubmit = async (data: FormData) => { const transformedData = prepareForUpdate(selectedUserResponders, selectedTeamResponder, data); - const resp = await directPagingStore.createManualAlertRule(transformedData); if (!resp) { @@ -65,13 +65,14 @@ export const ManualAlertGroup: FC = observer(({ onCreate, onHide(); }; - const utils = useStyles2(getUtilStyles); + const utilStyles = useStyles2(getUtilStyles); + const styles = useStyles2(getStyles); return ( -
+ = observer(({ onCreate, )} /> + -
+ +
- {!token && ( - - )} - - + + + + +
+ {renderTokenInput()} + {renderCopyToClipboard()} +
+ + {renderCurlExample()} + + + + + + + + +
+ +
); function renderTokenInput() { - return token ? ( - - ) : ( - ( + + <> + {token ? ( + + ) : ( + + )} + + + )} /> ); } @@ -104,6 +125,16 @@ export const ApiTokenForm = observer((props: TokenCreationModalProps) => { ); } + + async function onCreateTokenCallback() { + try { + const data = await store.apiTokenStore.create({ name }); + setToken(data.token); + onUpdate(); + } catch (error) { + openErrorNotification(get(error, 'response.data.detail', 'error creating token')); + } + } }); function getCurlExample(token, onCallApiUrl) { diff --git a/src/containers/EscalationChainForm/EscalationChainForm.tsx b/src/containers/EscalationChainForm/EscalationChainForm.tsx index ec694f7b0f..a2fb59d4da 100644 --- a/src/containers/EscalationChainForm/EscalationChainForm.tsx +++ b/src/containers/EscalationChainForm/EscalationChainForm.tsx @@ -1,8 +1,9 @@ -import React, { ChangeEvent, FC, useCallback, useState } from 'react'; +import React, { FC } from 'react'; import { Button, Field, HorizontalGroup, Input, Modal } from '@grafana/ui'; import cn from 'classnames/bind'; import { observer } from 'mobx-react'; +import { Controller, FormProvider, useForm } from 'react-hook-form'; import { GSelect } from 'containers/GSelect/GSelect'; import { EscalationChain } from 'models/escalation_chain/escalation_chain.types'; @@ -25,6 +26,11 @@ interface EscalationChainFormProps { onSubmit: (id: EscalationChain['id']) => Promise; } +interface EscalationFormFields { + team: string; + name: string; +} + const cx = cn.bind(styles); export const EscalationChainForm: FC = observer((props) => { @@ -40,96 +46,131 @@ export const EscalationChainForm: FC = observer((props } = store; const user = userStore.currentUser; - const escalationChain = escalationChainId ? escalationChainStore.items[escalationChainId] : undefined; + const isCreateMode = mode === EscalationChainFormMode.Create; + const isCopyMode = mode === EscalationChainFormMode.Copy; + + const formMethods = useForm({ + mode: 'onChange', + defaultValues: { + team: escalationChain?.team || user.current_team, + name: isCopyMode ? `${escalationChain.name} copy` : '', + }, + }); - const [name, setName] = useState( - mode === EscalationChainFormMode.Copy ? `${escalationChain?.name} copy` : escalationChain?.name + const { + control, + setError, + getValues, + formState: { errors }, + handleSubmit, + } = formMethods; + + return ( + +
+ +
+ ( + + + {...field} + items={grafanaTeamItems} + fetchItemsFn={grafanaTeamStore.updateItems} + fetchItemFn={grafanaTeamStore.fetchItemById} + getSearchResult={grafanaTeamStore.getSearchResult} + displayField="name" + valueField="id" + allowClear + placeholder="Select a team" + className={cx('team-select')} + /> + + )} + /> + + ( + + + + )} + /> + + + + + + +
+
+
); - const [selectedTeam, setSelectedTeam] = useState(escalationChain?.team || user.current_team); - const [errors, setErrors] = useState<{ [key: string]: string }>({}); - const onSubmit = useCallback(async () => { + async function onSubmit() { let escalationChain: EscalationChain | void; - const isCreateMode = mode === EscalationChainFormMode.Create; - const isCopyMode = mode === EscalationChainFormMode.Copy; - - if (isCreateMode) { - escalationChain = await escalationChainStore.create({ name, team: selectedTeam }); - } else if (isCopyMode) { - escalationChain = await escalationChainStore.clone(escalationChainId, { name, team: selectedTeam }); - } else { - escalationChain = await escalationChainStore.update(escalationChainId, { - name, - team: selectedTeam, - }); - } - - if (!escalationChain) { - let verb: string; + const teamName = getValues('team'); + const escalationChainName = getValues('name'); + try { if (isCreateMode) { - verb = 'creating'; + escalationChain = await escalationChainStore.create({ + name: escalationChainName, + team: teamName, + }); } else if (isCopyMode) { - verb = 'copying'; + escalationChain = await escalationChainStore.clone(escalationChainId, { + name: escalationChainName, + team: teamName, + }); } else { - verb = 'updating'; + escalationChain = await escalationChainStore.update(escalationChainId, { + name: escalationChainName, + team: teamName, + }); } - openWarningNotification(`There was an issue ${verb} the escalation chain. Please try again`); - return; - } + if (!escalationChain) { + let verb: string; + + if (isCreateMode) { + verb = 'creating'; + } else if (isCopyMode) { + verb = 'copying'; + } else { + verb = 'updating'; + } + + openWarningNotification(`There was an issue ${verb} the escalation chain. Please try again`); + return; + } - try { await onSubmitProp(escalationChain.id); onHide(); } catch (err) { - setErrors({ - name: err.response.data.name || err.response.data.detail || err.response.data.non_field_errors, - }); + if (err?.response?.data) { + const keys = Object.keys(err.response.data); + keys.forEach((key: keyof EscalationFormFields) => { + const message = Array.isArray(err.response.data[key]) ? err.response.data[key][0] : err.response.data[key]; + setError('name', { message }); + }); + } } - }, [name, selectedTeam, mode, onSubmitProp]); - - const handleNameChange = useCallback((event: ChangeEvent) => { - setName(event.target.value); - }, []); - - return ( - -
- - - items={grafanaTeamItems} - fetchItemsFn={grafanaTeamStore.updateItems} - fetchItemFn={grafanaTeamStore.fetchItemById} - getSearchResult={grafanaTeamStore.getSearchResult} - displayField="name" - valueField="id" - allowClear - placeholder="Select a team" - className={cx('team-select')} - onChange={setSelectedTeam} - value={selectedTeam} - /> - - - - - - - - -
-
- ); + } });