From 1fa918e4f74844ba6cc1274fe189bd109f30cfcb Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Fri, 11 Oct 2024 11:01:35 +0200 Subject: [PATCH] feat($env): additional environments - API integration (#8424) Make API calls from "order environments" dialog, improve validation --- .../OrderEnvironments.test.tsx | 42 ++++++++++++++++ .../OrderEnvironments/OrderEnvironments.tsx | 39 ++++++++++++--- .../OrderEnvironmentsDialog.tsx | 48 ++++++++++++------- .../useOrderEnvironmentsApi.ts | 24 ++++++++++ frontend/src/openapi/models/index.ts | 1 + .../openapi/models/orderEnvironmentsSchema.ts | 13 +++++ 6 files changed, 145 insertions(+), 22 deletions(-) create mode 100644 frontend/src/component/environments/EnvironmentTable/OrderEnvironments/OrderEnvironments.test.tsx create mode 100644 frontend/src/hooks/api/actions/useOrderEnvironmentsApi/useOrderEnvironmentsApi.ts create mode 100644 frontend/src/openapi/models/orderEnvironmentsSchema.ts diff --git a/frontend/src/component/environments/EnvironmentTable/OrderEnvironments/OrderEnvironments.test.tsx b/frontend/src/component/environments/EnvironmentTable/OrderEnvironments/OrderEnvironments.test.tsx new file mode 100644 index 000000000000..49e7d9513192 --- /dev/null +++ b/frontend/src/component/environments/EnvironmentTable/OrderEnvironments/OrderEnvironments.test.tsx @@ -0,0 +1,42 @@ +import { screen, fireEvent, waitFor } from '@testing-library/react'; +import { render } from 'utils/testRenderer'; +import { OrderEnvironments } from './OrderEnvironments'; +import { testServerRoute, testServerSetup } from 'utils/testServer'; + +const server = testServerSetup(); + +const setupServerRoutes = (changeRequestsEnabled = true) => { + testServerRoute(server, '/api/admin/ui-config', { + environment: 'Pro', + flags: { + purchaseAdditionalEnvironments: true, + }, + }); +}; + +describe('OrderEnvironmentsDialog Component', () => { + test('should show error if environment name is empty', async () => { + setupServerRoutes(); + render(); + + await waitFor(async () => { + const openDialogButton = await screen.queryByRole('button', { + name: /view pricing/i, + }); + expect(openDialogButton).toBeInTheDocument(); + fireEvent.click(openDialogButton!); + }); + + const checkbox = screen.getByRole('checkbox', { + name: /i understand adding environments/i, + }); + fireEvent.click(checkbox); + + const submitButton = screen.getByRole('button', { name: /order/i }); + fireEvent.click(submitButton); + + expect( + screen.getByText(/environment name is required/i), + ).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/component/environments/EnvironmentTable/OrderEnvironments/OrderEnvironments.tsx b/frontend/src/component/environments/EnvironmentTable/OrderEnvironments/OrderEnvironments.tsx index c03cc1234209..4ba0cd907e59 100644 --- a/frontend/src/component/environments/EnvironmentTable/OrderEnvironments/OrderEnvironments.tsx +++ b/frontend/src/component/environments/EnvironmentTable/OrderEnvironments/OrderEnvironments.tsx @@ -4,6 +4,10 @@ import { useUiFlag } from 'hooks/useUiFlag'; import { PurchasableFeature } from './PurchasableFeature/PurchasableFeature'; import { OrderEnvironmentsDialog } from './OrderEnvironmentsDialog/OrderEnvironmentsDialog'; import { OrderEnvironmentsConfirmation } from './OrderEnvironmentsConfirmation/OrderEnvironmentsConfirmation'; +import { useFormErrors } from 'hooks/useFormErrors'; +import useToast from 'hooks/useToast'; +import { formatUnknownError } from 'utils/formatUnknownError'; +import { useOrderEnvironmentApi } from 'hooks/api/actions/useOrderEnvironmentsApi/useOrderEnvironmentsApi'; type OrderEnvironmentsProps = {}; @@ -17,18 +21,40 @@ export const OrderEnvironments: FC = () => { const isPurchaseAdditionalEnvironmentsEnabled = useUiFlag( 'purchaseAdditionalEnvironments', ); + const errors = useFormErrors(); + const { orderEnvironments } = useOrderEnvironmentApi(); + const { setToastData, setToastApiError } = useToast(); if (!isPro() || !isPurchaseAdditionalEnvironmentsEnabled) { return null; } - const onSubmit = (environments: string[]) => { - setPurchaseDialogOpen(false); - // TODO: API call - setConfirmationState({ - isOpen: true, - environmentsCount: environments.length, + const onSubmit = async (environments: string[]) => { + let hasErrors = false; + environments.forEach((environment, index) => { + const field = `environment-${index}`; + if (environment.trim() === '') { + errors.setFormError(field, 'Environment name is required'); + hasErrors = true; + } else { + errors.removeFormError(field); + } }); + + if (hasErrors) { + return; + } else { + try { + await orderEnvironments({ environments }); + setPurchaseDialogOpen(false); + setConfirmationState({ + isOpen: true, + environmentsCount: environments.length, + }); + } catch (error) { + setToastApiError(formatUnknownError(error)); + } + } }; return ( @@ -42,6 +68,7 @@ export const OrderEnvironments: FC = () => { open={purchaseDialogOpen} onClose={() => setPurchaseDialogOpen(false)} onSubmit={onSubmit} + errors={errors} /> void; onSubmit: (environments: string[]) => void; + errors?: IFormErrors; }; const StyledDialog = styled(Dialog)(({ theme }) => ({ @@ -75,11 +77,14 @@ export const OrderEnvironmentsDialog: FC = ({ open, onClose, onSubmit, + errors, }) => { const { trackEvent } = usePlausibleTracker(); const [selectedOption, setSelectedOption] = useState(OPTIONS[0]); const [costCheckboxChecked, setCostCheckboxChecked] = useState(false); - const [environmentNames, setEnvironmentNames] = useState([]); + const [environmentNames, setEnvironmentNames] = useState(['']); + + console.log({ environmentNames }); const trackEnvironmentSelect = () => { trackEvent('order-environments', { @@ -143,7 +148,10 @@ export const OrderEnvironmentsDialog: FC = ({ const value = Number.parseInt(option, 10); setSelectedOption(value); setEnvironmentNames((names) => - names.slice(0, value), + [...names, ...Array(value).fill('')].slice( + 0, + value, + ), ); trackEnvironmentSelect(); }} @@ -154,20 +162,28 @@ export const OrderEnvironmentsDialog: FC = ({ How would you like the environment {selectedOption > 1 ? 's' : ''} to be named? - {[...Array(selectedOption)].map((_, i) => ( - { - setEnvironmentNames((names) => { - const newValues = [...names]; - newValues[i] = event.target.value; - return newValues; - }); - }} - /> - ))} + {[...Array(selectedOption)].map((_, i) => { + const error = errors?.getFormError( + `environment-${i}`, + ); + + return ( + { + setEnvironmentNames((names) => { + const newValues = [...names]; + newValues[i] = event.target.value; + return newValues; + }); + }} + error={Boolean(error)} + errorText={error} + /> + ); + })} { + const { makeRequest, createRequest, errors, loading } = useAPI({ + propagateErrors: true, + }); + + const orderEnvironments = async (payload: OrderEnvironmentsSchema) => { + const req = createRequest('api/admin/order-environments', { + method: 'POST', + body: JSON.stringify(payload), + }); + + const res = await makeRequest(req.caller, req.id); + return res.json(); + }; + + return { + orderEnvironments, + errors, + loading, + }; +}; diff --git a/frontend/src/openapi/models/index.ts b/frontend/src/openapi/models/index.ts index 997af9ecd04d..04ae7de1c66b 100644 --- a/frontend/src/openapi/models/index.ts +++ b/frontend/src/openapi/models/index.ts @@ -862,6 +862,7 @@ export * from './oidcSettingsSchemaOneOfFour'; export * from './oidcSettingsSchemaOneOfFourDefaultRootRole'; export * from './oidcSettingsSchemaOneOfFourIdTokenSigningAlgorithm'; export * from './oidcSettingsSchemaOneOfIdTokenSigningAlgorithm'; +export * from './orderEnvironmentsSchema'; export * from './outdatedSdksSchema'; export * from './outdatedSdksSchemaSdksItem'; export * from './overrideSchema'; diff --git a/frontend/src/openapi/models/orderEnvironmentsSchema.ts b/frontend/src/openapi/models/orderEnvironmentsSchema.ts new file mode 100644 index 000000000000..dda45a8aa562 --- /dev/null +++ b/frontend/src/openapi/models/orderEnvironmentsSchema.ts @@ -0,0 +1,13 @@ +/** + * Generated by Orval + * Do not edit manually. + * See `gen:api` script in package.json + */ + +/** + * A request for hosted customers to order new environments in Unleash. + */ +export interface OrderEnvironmentsSchema { + /** An array of environment names to be ordered. */ + environments: string[]; +}