Skip to content

chore(site): remove user search service #9939

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

Merged
merged 1 commit into from
Oct 2, 2023
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
5 changes: 4 additions & 1 deletion site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,12 @@ export const getTokenConfig = async (): Promise<TypesGen.TokenConfig> => {

export const getUsers = async (
options: TypesGen.UsersRequest,
signal?: AbortSignal,
): Promise<TypesGen.GetUsersResponse> => {
const url = getURLWithSearchParams("/api/v2/users", options);
const response = await axios.get<TypesGen.GetUsersResponse>(url.toString());
const response = await axios.get<TypesGen.GetUsersResponse>(url.toString(), {
signal,
});
return response.data;
};

Expand Down
12 changes: 8 additions & 4 deletions site/src/api/queries/users.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { QueryClient } from "@tanstack/react-query";
import { QueryClient, QueryOptions } from "@tanstack/react-query";
import * as API from "api/api";
import { UpdateUserPasswordRequest, UsersRequest } from "api/typesGenerated";
import {
GetUsersResponse,
UpdateUserPasswordRequest,
UsersRequest,
} from "api/typesGenerated";

export const users = (req: UsersRequest) => {
export const users = (req: UsersRequest): QueryOptions<GetUsersResponse> => {
return {
queryKey: ["users", req],
queryFn: () => API.getUsers(req),
queryFn: ({ signal }) => API.getUsers(req, signal),
};
};

Expand Down
146 changes: 71 additions & 75 deletions site/src/components/UserAutocomplete/UserAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,15 @@ import CircularProgress from "@mui/material/CircularProgress";
import { makeStyles } from "@mui/styles";
import TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete";
import { useMachine } from "@xstate/react";
import { User } from "api/typesGenerated";
import { Avatar } from "components/Avatar/Avatar";
import { AvatarData } from "components/AvatarData/AvatarData";
import {
ChangeEvent,
ComponentProps,
FC,
useEffect,
useRef,
useState,
} from "react";
import { searchUserMachine } from "xServices/users/searchUserXService";
import { ChangeEvent, ComponentProps, FC, useState } from "react";
import Box from "@mui/material/Box";
import { useDebouncedFunction } from "hooks/debounce";
import { useQuery } from "@tanstack/react-query";
import { users } from "api/queries/users";
import { prepareQuery } from "utils/filters";

export type UserAutocompleteProps = {
value: User | null;
Expand All @@ -34,53 +28,57 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
size = "small",
}) => {
const styles = useStyles();
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false);
const [searchState, sendSearch] = useMachine(searchUserMachine);
const { searchResults } = searchState.context;
const [autoComplete, setAutoComplete] = useState<{
value: string;
open: boolean;
}>({
value: value?.email ?? "",
open: false,
});
const usersQuery = useQuery({
...users({
q: prepareQuery(encodeURI(autoComplete.value)),
limit: 25,
}),
enabled: autoComplete.open,
keepPreviousData: true,
});

// Seed list of options on the first page load if a user passes in a value.
// Since some organizations have long lists of users, we do not want to load
// all options on page load.
const onMountRef = useRef(value);
useEffect(() => {
const mountValue = onMountRef.current;
if (mountValue) {
sendSearch("SEARCH", { query: mountValue.email });
}

// This isn't in XState's docs, but its source code guarantees that the
// memory reference of sendSearch will stay stable across renders. This
// useEffect call will behave like an on-mount effect and will not ever need
// to resynchronize
}, [sendSearch]);

const { debounced: debouncedOnChange } = useDebouncedFunction(
const { debounced: debouncedInputOnChange } = useDebouncedFunction(
(event: ChangeEvent<HTMLInputElement>) => {
sendSearch("SEARCH", { query: event.target.value });
setAutoComplete((state) => ({
...state,
value: event.target.value,
}));
},
1000,
750,
);

return (
<Autocomplete
noOptionsText="Start typing to search..."
// Since the values are filtered by the API we don't need to filter them
// in the FE because it can causes some mismatches.
filterOptions={(user) => user}
noOptionsText="No users found"
className={className}
options={searchResults ?? []}
loading={searchState.matches("searching")}
options={usersQuery.data?.users ?? []}
loading={usersQuery.isLoading}
value={value}
id="user-autocomplete"
open={isAutocompleteOpen}
open={autoComplete.open}
onOpen={() => {
setIsAutocompleteOpen(true);
setAutoComplete((state) => ({
...state,
open: true,
}));
}}
onClose={() => {
setIsAutocompleteOpen(false);
setAutoComplete({
value: value?.email ?? "",
open: false,
});
}}
onChange={(_, newValue) => {
if (newValue === null) {
sendSearch("CLEAR_RESULTS");
}

onChange(newValue);
}}
isOptionEqualToValue={(option: User, value: User) =>
Expand All @@ -97,39 +95,37 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
</Box>
)}
renderInput={(params) => (
<>
<TextField
{...params}
fullWidth
size={size}
label={label}
placeholder="User email or username"
className={styles.textField}
InputProps={{
...params.InputProps,
onChange: debouncedOnChange,
startAdornment: value && (
<Avatar size="sm" src={value.avatar_url}>
{value.username}
</Avatar>
),
endAdornment: (
<>
{searchState.matches("searching") ? (
<CircularProgress size={16} />
) : null}
{params.InputProps.endAdornment}
</>
),
classes: {
root: styles.inputRoot,
},
}}
InputLabelProps={{
shrink: true,
}}
/>
</>
<TextField
{...params}
fullWidth
size={size}
label={label}
placeholder="User email or username"
className={styles.textField}
InputProps={{
...params.InputProps,
onChange: debouncedInputOnChange,
startAdornment: value && (
<Avatar size="sm" src={value.avatar_url}>
{value.username}
</Avatar>
),
endAdornment: (
<>
{usersQuery.isFetching && autoComplete.open ? (
<CircularProgress size={16} />
) : null}
{params.InputProps.endAdornment}
</>
),
classes: {
root: styles.inputRoot,
},
}}
InputLabelProps={{
shrink: true,
}}
/>
)}
/>
);
Expand Down
65 changes: 0 additions & 65 deletions site/src/xServices/users/searchUserXService.ts

This file was deleted.