mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
chore(site): remove user search service (#9939)
This commit is contained in:
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
@ -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) =>
|
||||
@ -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,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
@ -1,65 +0,0 @@
|
||||
import { getUsers } from "api/api";
|
||||
import { User } from "api/typesGenerated";
|
||||
import { queryToFilter } from "utils/filters";
|
||||
import { assign, createMachine } from "xstate";
|
||||
|
||||
export type AutocompleteEvent =
|
||||
| { type: "SEARCH"; query: string }
|
||||
| { type: "CLEAR_RESULTS" };
|
||||
|
||||
export const searchUserMachine = createMachine(
|
||||
{
|
||||
id: "searchUserMachine",
|
||||
predictableActionArguments: true,
|
||||
schema: {
|
||||
context: {} as {
|
||||
searchResults?: User[];
|
||||
},
|
||||
events: {} as AutocompleteEvent,
|
||||
services: {} as {
|
||||
searchUsers: {
|
||||
data: User[];
|
||||
};
|
||||
},
|
||||
},
|
||||
context: {
|
||||
searchResults: [],
|
||||
},
|
||||
tsTypes: {} as import("./searchUserXService.typegen").Typegen0,
|
||||
initial: "idle",
|
||||
states: {
|
||||
idle: {
|
||||
on: {
|
||||
SEARCH: "searching",
|
||||
CLEAR_RESULTS: {
|
||||
actions: ["clearResults"],
|
||||
target: "idle",
|
||||
},
|
||||
},
|
||||
},
|
||||
searching: {
|
||||
invoke: {
|
||||
src: "searchUsers",
|
||||
onDone: {
|
||||
target: "idle",
|
||||
actions: ["assignSearchResults"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
services: {
|
||||
searchUsers: async (_, { query }) =>
|
||||
(await getUsers(queryToFilter(query))).users,
|
||||
},
|
||||
actions: {
|
||||
assignSearchResults: assign({
|
||||
searchResults: (_, { data }) => data,
|
||||
}),
|
||||
clearResults: assign({
|
||||
searchResults: (_) => undefined,
|
||||
}),
|
||||
},
|
||||
},
|
||||
);
|
Reference in New Issue
Block a user