Skip to content

Commit c08b7fe

Browse files
chore: TiCS maintenance (#5853)
1 parent ea056c3 commit c08b7fe

File tree

73 files changed

+658
-482
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+658
-482
lines changed

docs/component-standards/api-hooks.md

Lines changed: 44 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
- Auto-generated SDK lives in `src/app/apiclient`
66
- Manually write hooks in `src/app/api/query/` using the generated react-query.ts hooks
7-
- Use `useWebsocketAwareQuery` for queries to auto-invalidate on websocket updates
8-
- Use `useMutation` with `invalidateQueries` in `onSuccess` for mutations
7+
- Use `useWebsocketAwareQuery` with `queryOptionsWithHeaders` for queries to auto-invalidate on websocket updates and include response headers
8+
- Use `useMutation` with `mutationOptionsWithHeaders` and `invalidateQueries` in `onSuccess` for mutations
99
- Create mock resolvers in `src/testing/resolvers/` using MSW (Mock Service Worker)
1010
- Test hooks using `renderHookWithProviders` and mock resolvers
1111
- Follow naming convention: `use<Resource>` for list queries, `use<Action><Resource>` for single item and operation queries
@@ -22,16 +22,16 @@ Our API layer follows a three-tier architecture:
2222

2323
### Basic List Query
2424

25-
Use `useWebsocketAwareQuery` to automatically invalidate when websocket updates occur:
25+
Use `useWebsocketAwareQuery` with `queryOptionsWithHeaders` to automatically invalidate when websocket updates occur and include response headers:
2626

2727
```typescript
2828
export const useUsers = (options?: Options<ListUsersWithSummaryData>) => {
2929
return useWebsocketAwareQuery(
30-
listUsersWithSummaryOptions(options) as UseQueryOptions<
31-
ListUsersWithSummaryData,
32-
ListUsersWithSummaryError,
33-
ListUsersWithSummaryResponse
34-
>
30+
queryOptionsWithHeaders<
31+
ListUsersWithSummaryResponses,
32+
ListUsersWithSummaryErrors,
33+
ListUsersWithSummaryData
34+
>(options, listUsersWithSummary, listUsersWithSummaryQueryKey(options))
3535
);
3636
};
3737
```
@@ -41,11 +41,11 @@ export const useUsers = (options?: Options<ListUsersWithSummaryData>) => {
4141
```typescript
4242
export const useGetUser = (options: Options<GetUserData>) => {
4343
return useWebsocketAwareQuery(
44-
getUserOptions(options) as UseQueryOptions<
45-
GetUserData,
46-
GetUserError,
47-
GetUserResponse
48-
>
44+
queryOptionsWithHeaders<GetUserResponses, GetUserErrors, GetUserData>(
45+
options,
46+
getUser,
47+
getUserQueryKey(options)
48+
)
4949
);
5050
};
5151
```
@@ -57,31 +57,31 @@ For queries that transform data, use the `select` option:
5757
```typescript
5858
export const useUserCount = (options?: Options<ListUsersWithSummaryData>) => {
5959
return useWebsocketAwareQuery({
60-
...listUsersWithSummaryOptions(options),
60+
...queryOptionsWithHeaders<
61+
ListUsersWithSummaryResponses,
62+
ListUsersWithSummaryErrors,
63+
ListUsersWithSummaryData
64+
>(options, listUsersWithSummary, listUsersWithSummaryQueryKey(options)),
6165
select: (data) => data?.total ?? 0,
62-
} as UseQueryOptions<
63-
ListUsersWithSummaryResponse,
64-
ListUsersWithSummaryError,
65-
number
66-
>);
66+
});
6767
};
6868
```
6969

7070
## Writing Mutation Hooks
7171

7272
### Basic Mutation
7373

74-
Always invalidate related queries in `onSuccess`:
74+
Always invalidate related queries in `onSuccess`. Use `mutationOptionsWithHeaders` to include response headers:
7575

7676
```typescript
7777
export const useCreateUser = (mutationOptions?: Options<CreateUserData>) => {
7878
const queryClient = useQueryClient();
79-
return useMutation<
80-
CreateUserResponse,
81-
CreateUserError,
82-
Options<CreateUserData>
83-
>({
84-
...createUserMutation(mutationOptions),
79+
return useMutation({
80+
...mutationOptionsWithHeaders<
81+
CreateUserResponses,
82+
CreateUserErrors,
83+
CreateUserData
84+
>(mutationOptions, createUser),
8585
onSuccess: () => {
8686
return queryClient.invalidateQueries({
8787
queryKey: listUsersWithSummaryQueryKey(),
@@ -96,12 +96,12 @@ export const useCreateUser = (mutationOptions?: Options<CreateUserData>) => {
9696
```typescript
9797
export const useUpdateUser = (mutationOptions?: Options<UpdateUserData>) => {
9898
const queryClient = useQueryClient();
99-
return useMutation<
100-
UpdateUserResponse,
101-
UpdateUserError,
102-
Options<UpdateUserData>
103-
>({
104-
...updateUserMutation(mutationOptions),
99+
return useMutation({
100+
...mutationOptionsWithHeaders<
101+
UpdateUserResponses,
102+
UpdateUserErrors,
103+
UpdateUserData
104+
>(mutationOptions, updateUser),
105105
onSuccess: () => {
106106
return queryClient.invalidateQueries({
107107
queryKey: listUsersWithSummaryQueryKey(),
@@ -116,12 +116,12 @@ export const useUpdateUser = (mutationOptions?: Options<UpdateUserData>) => {
116116
```typescript
117117
export const useDeleteUser = (mutationOptions?: Options<DeleteUserData>) => {
118118
const queryClient = useQueryClient();
119-
return useMutation<
120-
DeleteUserResponse,
121-
DeleteUserError,
122-
Options<DeleteUserData>
123-
>({
124-
...deleteUserMutation(mutationOptions),
119+
return useMutation({
120+
...mutationOptionsWithHeaders<
121+
DeleteUserResponses,
122+
DeleteUserErrors,
123+
DeleteUserData
124+
>(mutationOptions, deleteUser),
125125
onSuccess: () => {
126126
return queryClient.invalidateQueries({
127127
queryKey: listUsersWithSummaryQueryKey(),
@@ -418,13 +418,14 @@ describe("useGetUser", () => {
418418

419419
3. **WebSocket Awareness**:
420420

421-
- Use `useWebsocketAwareQuery` for all queries to automatically invalidate on real-time updates
421+
- Use `useWebsocketAwareQuery` with `queryOptionsWithHeaders` for all queries to automatically invalidate on real-time updates
422422
- Map websocket models to query keys in `base.ts` if needed
423423

424424
4. **Type Safety**:
425425

426426
- Always use the generated types from `@/app/apiclient`
427-
- Cast query options to proper types with `as UseQueryOptions`
427+
- Use `queryOptionsWithHeaders` and `mutationOptionsWithHeaders` with proper type parameters for automatic type inference
428+
- Type parameters follow the pattern: `<Responses, Errors, Data>`
428429

429430
5. **Error Handling**:
430431

@@ -446,8 +447,9 @@ describe("useGetUser", () => {
446447
## Common Pitfalls
447448

448449
- **Don't** forget to invalidate queries after mutations - stale data is confusing for users
449-
- **Don't** use `useQuery` directly - always use `useWebsocketAwareQuery` for queries
450+
- **Don't** use `useQuery` directly - always use `useWebsocketAwareQuery` with `queryOptionsWithHeaders` for queries
451+
- **Don't** use `useMutation` directly without `mutationOptionsWithHeaders` - you'll miss response headers
450452
- **Don't** forget to export mock data from resolver files
451453
- **Don't** forget to set `resolved` flag in resolvers - it's useful for testing
452454
- **Don't** forget to handle empty states in tests
453-
- **Don't** forget to type cast query options - TypeScript won't catch type mismatches otherwise
455+
- **Don't** forget to provide all three type parameters (`Responses`, `Errors`, `Data`) to the utility functions

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ export default tseslint.config(
290290
"@typescript-eslint/sort-type-constituents": "error",
291291
// "@typescript-eslint/strict-boolean-expressions": "error",
292292
// "@typescript-eslint/switch-exhaustiveness-check": "error",
293+
"@typescript-eslint/use-unknown-in-catch-callback-variable": "error",
293294
},
294295
},
295296
{

src/app/api/query/base.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -78,22 +78,28 @@ export const useWebsocketAwareQuery = <
7878
const previousConnectedCount = usePrevious(connectedCount);
7979

8080
useEffect(() => {
81-
if (connectedCount !== previousConnectedCount) {
82-
queryClient.invalidateQueries({ queryKey: options?.queryKey });
83-
}
81+
const invalidate = async () => {
82+
if (connectedCount !== previousConnectedCount) {
83+
await queryClient.invalidateQueries({ queryKey: options?.queryKey });
84+
}
85+
};
86+
87+
void invalidate();
8488
}, [connectedCount, previousConnectedCount, queryClient, options]);
8589

8690
useEffect(() => {
87-
return subscribe(({ name: model }: { name: WebSocketEndpointModel }) => {
88-
// This mapped key is the key for the websocket notifications
89-
// TODO: replace with a function call to deduce the key/condition using the parameters
90-
const mappedKey = wsToQueryKeyMapping[model];
91-
const modelQueryKey = options?.queryKey[0];
92-
93-
if (mappedKey && mappedKey === modelQueryKey) {
94-
queryClient.invalidateQueries({ queryKey: options?.queryKey });
91+
return subscribe(
92+
async ({ name: model }: { name: WebSocketEndpointModel }) => {
93+
// This mapped key is the key for the websocket notifications
94+
// TODO: replace with a function call to deduce the key/condition using the parameters
95+
const mappedKey = wsToQueryKeyMapping[model];
96+
const modelQueryKey = options?.queryKey[0];
97+
98+
if (mappedKey && mappedKey === modelQueryKey) {
99+
await queryClient.invalidateQueries({ queryKey: options?.queryKey });
100+
}
95101
}
96-
});
102+
);
97103
}, [queryClient, subscribe, options]);
98104

99105
return useQuery<TQueryFnData, TError, TData>(options!);

src/app/base/components/ActionForm/ActionForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,11 @@ const ActionForm = <V extends object, E = null>({
9191
return (
9292
<FormikForm<V, E>
9393
onSubmit={(values, formikHelpers) => {
94-
onSubmit(values, formikHelpers);
9594
// Set selected count in component state once form is submitted, so
9695
// that the saving label is not affected by updates to the component's
9796
// selectedCount prop, e.g. unselecting or deleting items.
9897
setSelectedOnSubmit(selectedCount);
98+
return onSubmit(values, formikHelpers);
9999
}}
100100
savingLabel={
101101
showProcessingCount

src/app/base/components/DhcpFormFields/DhcpFormFields.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,16 +141,20 @@ export const DhcpFormFields = ({ editing }: Props): React.ReactElement => {
141141
name="type"
142142
onChange={(e: React.FormEvent) => {
143143
formikProps.handleChange(e);
144-
formikProps.setFieldValue("entity", "").catch((reason) => {
145-
throw new FormikFieldChangeError("entity", "setFieldValue", reason);
144+
formikProps.setFieldValue("entity", "").catch((reason: unknown) => {
145+
throw new FormikFieldChangeError(
146+
"entity",
147+
"setFieldValue",
148+
reason as string
149+
);
146150
});
147151
formikProps
148152
.setFieldTouched("entity", false, false)
149-
.catch((reason) => {
153+
.catch((reason: unknown) => {
150154
throw new FormikFieldChangeError(
151155
"entity",
152156
"setFieldTouched",
153-
reason
157+
reason as string
154158
);
155159
});
156160
}}

src/app/base/components/DhcpFormFields/MachineSelect/MachineSelect.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ export const MachineSelect = ({
5252
);
5353
const handleSelect = (machine: Machine | null) => {
5454
handleClose();
55-
setFieldValue(name, machine?.system_id || null).catch((reason) => {
56-
throw new FormikFieldChangeError(name, "setFieldValue", reason);
55+
setFieldValue(name, machine?.system_id || null).catch((reason: unknown) => {
56+
throw new FormikFieldChangeError(name, "setFieldValue", reason as string);
5757
});
5858
};
5959
useOnEscapePressed(handleClose);
@@ -81,11 +81,11 @@ export const MachineSelect = ({
8181
onClick={() => {
8282
setIsOpen(!isOpen);
8383
if (!isOpen) {
84-
setFieldValue(name, "", false).catch((reason) => {
84+
setFieldValue(name, "", false).catch((reason: unknown) => {
8585
throw new FormikFieldChangeError(
8686
name,
8787
"setFieldValue",
88-
reason
88+
reason as string
8989
);
9090
});
9191
}

src/app/base/components/DynamicSelect/DynamicSelect.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,12 @@ export const DynamicSelect = <V extends FormValues = FormValues>({
7979
name,
8080
currentOptionValues.length > 0 ? currentOptionValues[0] : "",
8181
false
82-
).catch((reason) => {
83-
throw new FormikFieldChangeError(name, "setFieldValue", reason);
82+
).catch((reason: unknown) => {
83+
throw new FormikFieldChangeError(
84+
name,
85+
"setFieldValue",
86+
reason as string
87+
);
8488
});
8589
}
8690
}

src/app/base/components/MacAddressField/MacAddressField.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@ export const MacAddressField = ({
1919
name={name}
2020
onChange={(evt: React.ChangeEvent<HTMLInputElement>) => {
2121
setFieldValue(name, formatMacAddress(evt.target.value)).catch(
22-
(reason) => {
23-
throw new FormikFieldChangeError(name, "setFieldValue", reason);
22+
(reason: unknown) => {
23+
throw new FormikFieldChangeError(
24+
name,
25+
"setFieldValue",
26+
reason as string
27+
);
2428
}
2529
);
2630
}}

src/app/base/components/NodeConfigurationFields/NodeConfigurationFields.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,11 @@ const NodeConfigurationFields = (): React.ReactElement => {
8181
}}
8282
onTagCreated={(tag) => {
8383
setFieldValue("tags", values.tags.concat([tag.id])).catch(
84-
(reason) => {
84+
(reason: unknown) => {
8585
throw new FormikFieldChangeError(
8686
"tags",
8787
"setFieldValue",
88-
reason
88+
reason as string
8989
);
9090
}
9191
);

src/app/base/components/PowerTypeFields/BasePowerField/BasePowerField.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,15 @@ export const BasePowerField = <V extends AnyObject>({
4444
const newFieldValue = checked
4545
? fieldValue.filter((val) => val !== checkboxValue)
4646
: [...fieldValue, checkboxValue];
47-
setFieldValue(fieldName, newFieldValue).catch((reason) => {
48-
throw new FormikFieldChangeError(
49-
fieldName,
50-
"setFieldValue",
51-
reason
52-
);
53-
});
47+
setFieldValue(fieldName, newFieldValue).catch(
48+
(reason: unknown) => {
49+
throw new FormikFieldChangeError(
50+
fieldName,
51+
"setFieldValue",
52+
reason as string
53+
);
54+
}
55+
);
5456
}}
5557
type="checkbox"
5658
value={checkboxValue}

0 commit comments

Comments
 (0)