Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add error message to swarm form #2930

Merged
merged 1 commit into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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