Skip to content

Commit

Permalink
Merge pull request #2930 from andrewbaldwin44/feature/swarm-form-errors
Browse files Browse the repository at this point in the history
Add error message to swarm form
  • Loading branch information
cyberw authored Oct 8, 2024
2 parents 80a1c01 + 1e733f0 commit e052020
Show file tree
Hide file tree
Showing 13 changed files with 109 additions and 72 deletions.
8 changes: 7 additions & 1 deletion locust/webui/src/components/Form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ interface IForm<IInputData extends BaseInputData> {
children: React.ReactElement | React.ReactElement[];
className?: string;
onSubmit: (inputData: IInputData) => void;
onChange?: (formEvent: React.ChangeEvent<HTMLFormElement>) => void;
}

const FORM_INPUT_ELEMENTS = 'input, select, textarea';
Expand All @@ -32,6 +33,7 @@ const getInputValue = (inputElement: HTMLInputElement | HTMLSelectElement) => {
export default function Form<IInputData extends BaseInputData>({
children,
onSubmit,
onChange,
}: IForm<IInputData>) {
const formSubmitHandler = useCallback(
async (event: FormEvent<HTMLFormElement>) => {
Expand All @@ -53,5 +55,9 @@ export default function Form<IInputData extends BaseInputData>({
[onSubmit],
);

return <form onSubmit={formSubmitHandler}>{children}</form>;
return (
<form onChange={onChange} onSubmit={formSubmitHandler}>
{children}
</form>
);
}
2 changes: 1 addition & 1 deletion locust/webui/src/components/Layout/Navbar/SwarmMonitor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { Box, Divider, Tooltip, Typography } from '@mui/material';
import { connect } from 'react-redux';

import { SWARM_STATE } from 'constants/swarm';
import { ISwarmState } from 'redux/slice/swarm.slice';
import { IUiState } from 'redux/slice/ui.slice';
import { IRootState } from 'redux/store';
import { ISwarmState } from 'types/swarm.types';

interface ISwarmMonitor
extends Pick<ISwarmState, 'isDistributed' | 'host' | 'state' | 'workerCount'>,
Expand Down
2 changes: 1 addition & 1 deletion locust/webui/src/components/Reports/Reports.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { connect } from 'react-redux';

import { THEME_MODE } from 'constants/theme';
import { useSelector } from 'redux/hooks';
import { ISwarmState } from 'redux/slice/swarm.slice';
import { IRootState } from 'redux/store';
import { ISwarmState } from 'types/swarm.types';

function Reports({
extendedCsvFiles,
Expand Down
5 changes: 2 additions & 3 deletions locust/webui/src/components/SwarmForm/SwarmEditForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import { connect } from 'react-redux';

import Form from 'components/Form/Form';
import { useStartSwarmMutation } from 'redux/api/swarm';
import { ISwarmState, swarmActions } from 'redux/slice/swarm.slice';
import { swarmActions } from 'redux/slice/swarm.slice';
import { IRootState } from 'redux/store';

type ISwarmFormInput = Pick<ISwarmState, 'spawnRate' | 'userCount'>;
import { ISwarmFormInput, ISwarmState } from 'types/swarm.types';

interface ISwarmForm extends ISwarmFormInput {
onSubmit: () => void;
Expand Down
53 changes: 34 additions & 19 deletions locust/webui/src/components/SwarmForm/SwarmForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,11 @@ import CustomParameters from 'components/SwarmForm/SwarmCustomParameters';
import SwarmUserClassPicker from 'components/SwarmForm/SwarmUserClassPicker';
import { SWARM_STATE } from 'constants/swarm';
import { useStartSwarmMutation } from 'redux/api/swarm';
import { swarmActions, ISwarmState } from 'redux/slice/swarm.slice';
import { swarmActions } from 'redux/slice/swarm.slice';
import { IRootState } from 'redux/store';
import { ISwarmFormInput, ISwarmState } from 'types/swarm.types';
import { isEmpty } from 'utils/object';

interface ISwarmFormInput extends Pick<ISwarmState, 'host' | 'spawnRate' | 'userCount'> {
runTime: string;
userClasses: string[];
shapeClass: string;
}

interface IDispatchProps {
setSwarm: (swarmPayload: Partial<ISwarmState>) => void;
}
Expand All @@ -54,6 +49,7 @@ interface ISwarmForm
message: string;
};
isDisabled?: boolean;
onFormChange?: (formData: React.ChangeEvent<HTMLFormElement>) => void;
}

function SwarmForm({
Expand All @@ -70,23 +66,39 @@ function SwarmForm({
spawnRate,
alert,
isDisabled = false,
onFormChange,
}: ISwarmForm) {
const [startSwarm] = useStartSwarmMutation();
const [errorMessage, setErrorMessage] = useState('');
const [selectedUserClasses, setSelectedUserClasses] = useState(availableUserClasses);

const onStartSwarm = (inputData: ISwarmFormInput) => {
setSwarm({
state: SWARM_STATE.RUNNING,
host: inputData.host || host,
runTime: inputData.runTime,
spawnRate: Number(inputData.spawnRate) || null,
numUsers: Number(inputData.userCount) || null,
});

startSwarm({
const onStartSwarm = async (inputData: ISwarmFormInput) => {
const { data } = await startSwarm({
...inputData,
...(showUserclassPicker && selectedUserClasses ? { userClasses: selectedUserClasses } : {}),
});

if (data && data.success) {
setSwarm({
state: SWARM_STATE.RUNNING,
host: inputData.host || host,
runTime: inputData.runTime,
spawnRate: Number(inputData.spawnRate) || null,
numUsers: Number(inputData.userCount) || null,
});
} else {
setErrorMessage(data ? data.message : 'An unknown error occured.');
}
};

const handleSwarmFormChange = (formEvent: React.ChangeEvent<HTMLFormElement>) => {
if (errorMessage) {
setErrorMessage('');
}

if (onFormChange) {
onFormChange(formEvent);
}
};

return (
Expand All @@ -103,7 +115,7 @@ function SwarmForm({
/>
</Box>
)}
<Form<ISwarmFormInput> onSubmit={onStartSwarm}>
<Form<ISwarmFormInput> onChange={handleSwarmFormChange} onSubmit={onStartSwarm}>
<Box
sx={{
marginBottom: 2,
Expand Down Expand Up @@ -153,7 +165,10 @@ function SwarmForm({
</AccordionDetails>
</Accordion>
{!isEmpty(extraOptions) && <CustomParameters extraOptions={extraOptions} />}
{alert && <Alert severity={alert.level || 'info'}>{alert.message}</Alert>}
{alert && !errorMessage && (
<Alert severity={alert.level || 'info'}>{alert.message}</Alert>
)}
{errorMessage && <Alert severity={'error'}>{errorMessage}</Alert>}
<Button disabled={isDisabled} size='large' type='submit' variant='contained'>
Start
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import Form from 'components/Form/Form';
import Select from 'components/Form/Select';
import Modal from 'components/Modal/Modal';
import { useUpdateUserSettingsMutation } from 'redux/api/swarm';
import { ISwarmState, swarmActions } from 'redux/slice/swarm.slice';
import { swarmActions } from 'redux/slice/swarm.slice';
import { IRootState } from 'redux/store';
import { ISwarmUser } from 'types/swarm.types';
import { ISwarmState, ISwarmUser } from 'types/swarm.types';
import { toTitleCase } from 'utils/string';

interface IDispatchProps {
Expand Down
3 changes: 1 addition & 2 deletions locust/webui/src/constants/swarm.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ISwarmState } from 'redux/slice/swarm.slice';
import { IReport, IReportTemplateArgs } from 'types/swarm.types';
import { IReport, IReportTemplateArgs, ISwarmState } from 'types/swarm.types';
import { ICharts } from 'types/ui.types';
import { updateArraysAtProps } from 'utils/object';
import { camelCaseKeys } from 'utils/string';
Expand Down
9 changes: 8 additions & 1 deletion locust/webui/src/redux/api/swarm.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

import { ISwarmFormInput } from 'types/swarm.types';
import {
IStatsResponse,
ISwarmExceptionsResponse,
Expand All @@ -9,6 +10,12 @@ import {
import { createFormData } from 'utils/object';
import { camelCaseKeys, snakeCaseKeys } from 'utils/string';

interface IStartSwarmResponse {
success: boolean;
message: string;
host: string;
}

export const api = createApi({
baseQuery: fetchBaseQuery(),
endpoints: builder => ({
Expand All @@ -29,7 +36,7 @@ export const api = createApi({
transformResponse: camelCaseKeys<ILogsResponse>,
}),

startSwarm: builder.mutation({
startSwarm: builder.mutation<IStartSwarmResponse, ISwarmFormInput>({
query: body => ({
url: 'swarm',
method: 'POST',
Expand Down
3 changes: 2 additions & 1 deletion locust/webui/src/redux/slice/root.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import notification, {
INotificationState,
NotificationAction,
} from 'redux/slice/notification.slice';
import swarm, { ISwarmState, SwarmAction } from 'redux/slice/swarm.slice';
import swarm, { SwarmAction } from 'redux/slice/swarm.slice';
import theme, { IThemeState, ThemeAction } from 'redux/slice/theme.slice';
import ui, { IUiState, UiAction } from 'redux/slice/ui.slice';
import url, { IUrlState, UrlAction } from 'redux/slice/url.slice';
import { ISwarmState } from 'types/swarm.types';

export interface IRootState {
logViewer: ILogViewerState;
Expand Down
33 changes: 1 addition & 32 deletions locust/webui/src/redux/slice/swarm.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';

import { swarmTemplateArgs } from 'constants/swarm';
import { updateStateWithPayload } from 'redux/utils';
import { IExtraOptions, IHistory, ISwarmUser } from 'types/swarm.types';
import { ITab } from 'types/tab.types';
import { ITableStructure } from 'types/table.types';

export interface ISwarmState {
availableShapeClasses: string[];
availableUserClasses: string[];
availableUserTasks: { [key: string]: string[] };
extraOptions: IExtraOptions;
extendedTabs?: ITab[];
extendedTables?: { key: string; structure: ITableStructure[] }[];
extendedCsvFiles?: { href: string; title: string }[];
history: IHistory[];
host: string;
isDistributed: boolean;
isShape: boolean | null;
locustfile: string;
numUsers: number | null;
overrideHostWarning: boolean;
percentilesToChart: number[];
percentilesToStatistics: number[];
runTime?: string | number;
showUserclassPicker: boolean;
spawnRate: number | null;
state: string;
statsHistoryEnabled: boolean;
tasks: string;
userCount: number | string;
users: { [key: string]: ISwarmUser };
version: string;
workerCount: number;
}
import { ISwarmState } from 'types/swarm.types';

export type SwarmAction = PayloadAction<Partial<ISwarmState>>;

Expand Down
50 changes: 44 additions & 6 deletions locust/webui/src/types/swarm.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ITab } from 'types/tab.types';
import { ITableStructure } from 'types/table.types';
import {
ICharts,
ISwarmError,
Expand All @@ -18,7 +20,7 @@ export interface IExtraOptions {
[key: string]: IExtraOptionParameter;
}

export interface IHistory {
interface IHistory {
currentRps: [string, number];
currentFailPerSec: [string, number];
userCount: [string, number];
Expand All @@ -29,6 +31,42 @@ export interface IHistory {
time: string;
}

export interface ISwarmUser {
fixedCount: number;
host: string;
weight: number;
tasks: string[];
}

export interface ISwarmState {
availableShapeClasses: string[];
availableUserClasses: string[];
availableUserTasks: { [key: string]: string[] };
extraOptions: IExtraOptions;
extendedTabs?: ITab[];
extendedTables?: { key: string; structure: ITableStructure[] }[];
extendedCsvFiles?: { href: string; title: string }[];
history: IHistory[];
host: string;
isDistributed: boolean;
isShape: boolean | null;
locustfile: string;
numUsers: number | null;
overrideHostWarning: boolean;
percentilesToChart: number[];
percentilesToStatistics: number[];
runTime?: string | number;
showUserclassPicker: boolean;
spawnRate: number | null;
state: string;
statsHistoryEnabled: boolean;
tasks: string;
userCount: number | string;
users: { [key: string]: ISwarmUser };
version: string;
workerCount: number;
}

export interface IReport {
locustfile: string;
showDownloadLink: boolean;
Expand All @@ -50,9 +88,9 @@ export interface IReportTemplateArgs extends Omit<IReport, 'charts'> {
percentilesToStatistics: number[];
}

export interface ISwarmUser {
fixedCount: number;
host: string;
weight: number;
tasks: string[];
export interface ISwarmFormInput
extends Partial<Pick<ISwarmState, 'host' | 'spawnRate' | 'userCount'>> {
runTime?: string;
userClasses?: string[];
shapeClass?: string;
}
3 changes: 1 addition & 2 deletions locust/webui/src/types/window.types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { PaletteMode } from '@mui/material';

import type { ISwarmState } from 'redux/slice/swarm.slice';
import { IAuthArgs } from 'types/auth.types';
import { IReportTemplateArgs } from 'types/swarm.types';
import { IReportTemplateArgs, ISwarmState } from 'types/swarm.types';

export interface IWindow {
templateArgs: IReportTemplateArgs | ISwarmState;
Expand Down
6 changes: 5 additions & 1 deletion locust/webui/src/utils/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ export function shallowMerge<ObjectA, ObjectB>(objectA: ObjectA, objectB: Object
};
}

export const createFormData = (inputData: { [key: string]: string | string[] }) => {
export const createFormData = <
IInputdata extends Record<string, any> = { [key: string]: string | string[] },
>(
inputData: IInputdata,
) => {
const formData = new URLSearchParams();

for (const [key, value] of Object.entries(inputData)) {
Expand Down

0 comments on commit e052020

Please sign in to comment.