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[];
+}