Skip to content
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
1 change: 1 addition & 0 deletions .claude/rules/fleet-frontend.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Use helpers from `frontend/utilities/strings/stringUtils.ts`:
- Interface files live in `frontend/interfaces/` with `I` prefix: `IHost`, `IUser`, `IPack`
- Legacy pattern: some files export both PropTypes (default export) and TypeScript interfaces (named export)
- New code should use TypeScript interfaces only
- API interface naming: use `*FormData` for form-driven request bodies, `*ApiParams`/`*QueryParams` for request params, `*Response` for API responses, `*QueryKey` when typing a React Query key. Avoid `*Body`, `*PostBody`, `*Payload`, `*Request` for API request bodies. `*PreviewPayload` is fine for outgoing webhook shapes (matches the "Preview payload" UI terminology).

## Hooks & Context
- Custom hooks in `frontend/hooks/` — e.g., `useTeamIdParam`, `useCheckboxListStateManagement`
Expand Down
27 changes: 22 additions & 5 deletions frontend/docs/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,22 +94,39 @@ const functionWithTableName = (tableName: string)=> {

```typescript
// API interfaces should live in the relevant entities file.
// Their names should be named to clarify what they are used for when interacting
// with the API
// Their names should clarify what they are used for when interacting with the
// API. In service functions, prefer `formData` as the variable name for request
// bodies to stay consistent with the *FormData interface naming convention.

// should be defined in service/entities/hosts.ts
interface IHostDetailsReponse {
interface IHostDetailsResponse {
...
Comment on lines 101 to 103
}
interface IGetHostsQueryParams {
...
}

// should be defined in service/entities/fleets.ts
interface ICreateFleetPostBody {
// should be defined in service/entities/users.ts
interface IUpdateUserFormData {
...
}

// should be defined in service/entities/software.ts
interface IGetSoftwareApiParams {
...
}
interface ISoftwareCountResponse {
...
}

// Use *FormData for form-driven bodies, *ApiParams/*QueryParams for request
// params, *Response for responses, *QueryKey when typing a React Query key.
// Avoid *Body, *PostBody, *Payload, *Request for API request bodies — use
// *FormData instead, even for programmatic request bodies (e.g.
// IDeleteQueriesFormData). One consistent suffix is easier to follow than
// asking each dev to judge "is this form-driven enough?"
// *PreviewPayload is fine for outgoing webhook shapes (matches the
// "Preview payload" UI terminology).
```

## Utilities
Expand Down
2 changes: 1 addition & 1 deletion frontend/interfaces/mdm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ export type RecoveryLockPasswordStatus =
| "removing_enforcement"
| "failed";

export interface IMdmSSOReponse {
export interface IMdmSSOResponse {
url: string;
}

Expand Down
8 changes: 0 additions & 8 deletions frontend/interfaces/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,6 @@ import { IFormField } from "./form_field";
import { IPack } from "./pack";
import { ISchedulableQuery, ISchedulableQueryStats } from "./schedulable_query";

export interface IEditQueryFormData {
description?: string | number | boolean | undefined;
name?: string | number | boolean | undefined;
query?: string | number | boolean | undefined;
observer_can_run?: string | number | boolean | undefined;
automations_enabled?: boolean;
}

export interface IStoredQueryResponse {
query: ISchedulableQuery;
}
Expand Down
12 changes: 6 additions & 6 deletions frontend/interfaces/schedulable_query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export interface IQueryKeyQueriesLoadAll {
}
// Create a new query
/** POST /api/v1/fleet/queries */
export interface ICreateQueryRequestBody {
export interface ICreateQueryFormData {
name: string;
query: string;
description?: string;
Expand All @@ -121,10 +121,10 @@ export interface ICreateQueryRequestBody {

// response is ISchedulableQuery

// Modify a query by id
// Edit a query by id
/** PATCH /api/v1/fleet/queries/{id} */
export interface IModifyQueryRequestBody
extends Omit<ICreateQueryRequestBody, "name" | "query" | "fleet_id"> {
export interface IEditQueryFormData
extends Omit<ICreateQueryFormData, "name" | "query" | "fleet_id"> {
id?: number;
name?: string;
query?: string;
Expand All @@ -141,7 +141,7 @@ export interface IModifyQueryRequestBody

// Delete a query by name
/** DELETE /api/v1/fleet/queries/{name} */
export interface IDeleteQueryRequestBody {
export interface IDeleteQueryFormData {
fleet_id?: number; // searches for a global query if omitted
}

Expand All @@ -151,7 +151,7 @@ export interface IDeleteQueryRequestBody {

// Delete queries by id
/** POST /api/v1/fleet/queries/delete */
export interface IDeleteQueriesRequestBody {
export interface IDeleteQueriesFormData {
ids: number[];
}

Expand Down
8 changes: 4 additions & 4 deletions frontend/interfaces/team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,21 +114,21 @@ export interface INewTeamUser {
/**
* The shape of the body expected from the API when adding new users to teams
*/
export interface INewTeamUsersBody {
export interface INewTeamUsersFormData {
users: INewTeamUser[];
}
export interface IRemoveTeamUserBody {
export interface IRemoveTeamUserFormData {
users: { id?: number }[];
}
interface INewTeamSecret {
team_id: number;
secret: string;
created_at?: string;
}
export interface INewTeamSecretBody {
export interface INewTeamSecretFormData {
secrets: INewTeamSecret[];
}
export interface IRemoveTeamSecretBody {
export interface IRemoveTeamSecretFormData {
secrets: { secret: string }[];
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/interfaces/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export interface IUser {
/**
* The shape of the request body when updating a user.
*/
export interface IUserUpdateBody {
export interface IUserUpdateFormData {
global_role?: UserRole | null;
teams?: ITeam[];
name: string;
Expand Down
2 changes: 1 addition & 1 deletion frontend/interfaces/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export interface IVariable {
updated_at: string;
}

export interface IVariablePayload {
export interface IVariableFormData {
name: string;
value: string;
}
2 changes: 1 addition & 1 deletion frontend/interfaces/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default PropTypes.shape({
build_user: PropTypes.string,
});

export interface IVersionData {
export interface IVersionResponse {
version: string;
branch: string;
revision: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useContext, useEffect, useState } from "react";

import { IUser } from "interfaces/user";
import { IVersionData } from "interfaces/version";
import { IVersionResponse } from "interfaces/version";

import { AppContext } from "context/app";

Expand Down Expand Up @@ -31,7 +31,7 @@ const AccountSidePanel = ({
onGetApiToken,
}: IAccountSidePanelProps): JSX.Element => {
const { isPremiumTier, config } = useContext(AppContext);
const [versionData, setVersionData] = useState<IVersionData>();
const [versionData, setVersionData] = useState<IVersionResponse>();
const [themeMode, setThemeModeState] = useState<ThemeMode>(() =>
getThemeMode()
);
Expand Down
6 changes: 3 additions & 3 deletions frontend/pages/ConfirmInvitePage/ConfirmInvitePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { NotificationContext } from "context/notification";
import { ICreateUserWithInvitationFormData } from "interfaces/user";
import paths from "router/paths";
import usersAPI from "services/entities/users";
import inviteAPI, { IValidateInviteResp } from "services/entities/invites";
import inviteAPI, { IValidateInviteResponse } from "services/entities/invites";

import AuthenticationFormWrapper from "components/AuthenticationFormWrapper";
import Spinner from "components/Spinner";
Expand Down Expand Up @@ -36,12 +36,12 @@ const ConfirmInvitePage = ({ router, params }: IConfirmInvitePageProps) => {
data: validInvite,
error: validateInviteError,
isLoading: isVerifyingInvite,
} = useQuery<IValidateInviteResp, AxiosError, IInvite>(
} = useQuery<IValidateInviteResponse, AxiosError, IInvite>(
"invite",
() => inviteAPI.verify(invite_token),
{
...DEFAULT_USE_QUERY_OPTIONS,
select: (resp: IValidateInviteResp) => resp.invite,
select: (resp: IValidateInviteResponse) => resp.invite,
}
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { AppContext } from "context/app";
import { NotificationContext } from "context/notification";
import usersAPI from "services/entities/users";
import sessionsAPI from "services/entities/sessions";
import inviteAPI, { IValidateInviteResp } from "services/entities/invites";
import inviteAPI, { IValidateInviteResponse } from "services/entities/invites";
import { IInvite } from "interfaces/invite";
import { getErrorReason } from "interfaces/errors";
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
Expand Down Expand Up @@ -43,12 +43,12 @@ const ConfirmSSOInvitePage = ({
data: validInvite,
error: validateInviteError,
isLoading: isVerifyingInvite,
} = useQuery<IValidateInviteResp, AxiosError, IInvite>(
} = useQuery<IValidateInviteResponse, AxiosError, IInvite>(
["invite", invite_token],
() => inviteAPI.verify(invite_token),
{
...DEFAULT_USE_QUERY_OPTIONS,
select: (resp: IValidateInviteResp) => resp.invite,
select: (resp: IValidateInviteResponse) => resp.invite,
}
);

Expand Down
4 changes: 2 additions & 2 deletions frontend/pages/DashboardPage/cards/ChartCard/ChartCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { SingleValue } from "react-select-5";

import chartsAPI, {
IChartResponse,
IChartRequestParams,
IChartApiParams,
IChartQueryKey,
} from "services/entities/charts";
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
Expand Down Expand Up @@ -150,7 +150,7 @@ const ChartCard = ({
? true
: historicalDataEnabled?.[datasetConfigKey] ?? true;

const queryParams: IChartRequestParams = useMemo(() => {
const queryParams: IChartApiParams = useMemo(() => {
return {
// Add an extra day to ensure we get the full # of calendar days
// represented in the chart, regardless of timezone.
Expand Down
4 changes: 2 additions & 2 deletions frontend/pages/MDMAppleSSOPage/MDMAppleSSOPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import SSOError from "components/MDM/SSOError";
import Spinner from "components/Spinner/Spinner";
import Button from "components/buttons/Button";
import CustomLink from "components/CustomLink";
import { IMdmSSOReponse } from "interfaces/mdm";
import { IMdmSSOResponse } from "interfaces/mdm";
import AuthenticationFormWrapper from "components/AuthenticationFormWrapper";

const baseClass = "mdm-apple-sso-page";
Expand All @@ -25,7 +25,7 @@ const DEPSSOLoginPage = ({
? "account_driven_enroll"
: "mdm_sso";
}
const { error } = useQuery<IMdmSSOReponse, AxiosError>(
const { error } = useQuery<IMdmSSOResponse, AxiosError>(
["dep_sso"],
() => mdmAPI.initiateMDMAppleSSO(query),
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useContext, useState } from "react";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
import { IVariablePayload } from "interfaces/variables";
import { IVariableFormData } from "interfaces/variables";
import { hasStatusKey } from "interfaces/errors";
import variablesAPI from "services/entities/variables";
import { NotificationContext } from "context/notification";
Expand Down Expand Up @@ -59,7 +59,7 @@ const AddCustomVariableModal = ({
const validation = validateFormData({ name, value }, true);
if (validation.isValid) {
setIsSaving(true);
const newVariable: IVariablePayload = {
const newVariable: IVariableFormData = {
name: variableName,
value: variableValue,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";

import { IAddCertAuthorityBody } from "services/entities/certificates";
import { IAddCertAuthorityFormData } from "services/entities/certificates";
import { ICertificateAuthorityType } from "interfaces/certificates";
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
import { IDropdownOption } from "interfaces/dropdownOption";
Expand Down Expand Up @@ -68,7 +68,7 @@ export const generateDropdownOptions = (hasNDESCert: boolean) => {
export const generateAddCertAuthorityData = (
certAuthorityType: ICertificateAuthorityType,
formData: ICertFormData
): IAddCertAuthorityBody | undefined => {
): IAddCertAuthorityFormData | undefined => {
switch (certAuthorityType) {
case "ndes_scep_proxy": {
const {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";

import { IEditCertAuthorityBody } from "services/entities/certificates";
import { IEditCertAuthorityFormData } from "services/entities/certificates";
import {
ICertificateAuthority,
ICertificatesCustomSCEP,
Expand Down Expand Up @@ -79,7 +79,7 @@ export const generateDefaultFormData = (
export const generateEditCertAuthorityData = (
certAuthority: ICertificateAuthority,
formData: ICertFormData
): IEditCertAuthorityBody => {
): IEditCertAuthorityFormData => {
const certAuthWithoutType = Object.assign({}, certAuthority);
delete certAuthWithoutType.type;
delete certAuthWithoutType.id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AppContext } from "context/app";
import { NotificationContext } from "context/notification";
import useTeamIdParam from "hooks/useTeamIdParam";
import { IApiError } from "interfaces/errors";
import { INewTeamUsersBody, ITeam } from "interfaces/team";
import { INewTeamUsersFormData, ITeam } from "interfaces/team";
import { IUpdateUserFormData, IUser, IUserFormErrors } from "interfaces/user";
import { ITeamSubnavProps } from "interfaces/team_subnav";
import PATHS from "router/paths";
Expand Down Expand Up @@ -172,7 +172,7 @@ const UsersPage = ({ location, router }: ITeamSubnavProps): JSX.Element => {
]);

const onAddUserSubmit = useCallback(
(newUsers: INewTeamUsersBody) => {
(newUsers: INewTeamUsersFormData) => {
teamsAPI
.addUsers(currentTeamDetails?.id, newUsers)
.then(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useCallback, useState } from "react";

import { INewTeamUser, INewTeamUsersBody, ITeam } from "interfaces/team";
import { INewTeamUser, INewTeamUsersFormData, ITeam } from "interfaces/team";
import endpoints from "utilities/endpoints";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
Expand All @@ -13,7 +13,7 @@ interface IAddUsersModal {
team: ITeam;
disabledUsers: number[];
onCancel: () => void;
onSubmit: (userIds: INewTeamUsersBody) => void;
onSubmit: (userIds: INewTeamUsersFormData) => void;
onCreateNewTeamUser: () => void;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { userTeamStub } from "test/stubs";
import createMockUser from "__mocks__/userMock";
import { IUserUpdateBody } from "interfaces/user";
import { IUserUpdateFormData } from "interfaces/user";

import { IUserFormData, NewUserType } from "../components/UserForm/UserForm";
import userManagementHelpers from "./userManagementHelpers";

describe("userManagementHelpers module", () => {
describe("generateUpdatedData function", () => {
it("returns an object with only the difference between the two", () => {
const updatedTeam: IUserUpdateBody = {
const updatedTeam: IUserUpdateFormData = {
...userTeamStub,
role: "maintainer",
};
const newTeam: IUserUpdateBody = {
const newTeam: IUserUpdateFormData = {
...userTeamStub,
id: 2,
role: "observer",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import scriptsAPI, {
IListScriptsQueryKey,
IScriptBatchSupportedFilters,
IScriptsResponse,
IRunScriptBatchRequest,
IRunScriptBatchFormData,
} from "services/entities/scripts";
import ScriptDetailsModal from "pages/hosts/components/ScriptDetailsModal";
import Spinner from "components/Spinner";
Expand Down Expand Up @@ -150,7 +150,7 @@ const RunScriptBatchModal = ({
setIsUpdating(true);

// Create the base request.
let body: IRunScriptBatchRequest;
let body: IRunScriptBatchFormData;

if (runByFilters) {
body = {
Expand Down
Loading
Loading