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 (
|
export const getUsers = async (
|
||||||
options: TypesGen.UsersRequest,
|
options: TypesGen.UsersRequest,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<TypesGen.GetUsersResponse> => {
|
): Promise<TypesGen.GetUsersResponse> => {
|
||||||
const url = getURLWithSearchParams("/api/v2/users", options);
|
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;
|
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 * 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 {
|
return {
|
||||||
queryKey: ["users", req],
|
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 { makeStyles } from "@mui/styles";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Autocomplete from "@mui/material/Autocomplete";
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
import { useMachine } from "@xstate/react";
|
|
||||||
import { User } from "api/typesGenerated";
|
import { User } from "api/typesGenerated";
|
||||||
import { Avatar } from "components/Avatar/Avatar";
|
import { Avatar } from "components/Avatar/Avatar";
|
||||||
import { AvatarData } from "components/AvatarData/AvatarData";
|
import { AvatarData } from "components/AvatarData/AvatarData";
|
||||||
import {
|
import { ChangeEvent, ComponentProps, FC, useState } from "react";
|
||||||
ChangeEvent,
|
|
||||||
ComponentProps,
|
|
||||||
FC,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { searchUserMachine } from "xServices/users/searchUserXService";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { useDebouncedFunction } from "hooks/debounce";
|
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 = {
|
export type UserAutocompleteProps = {
|
||||||
value: User | null;
|
value: User | null;
|
||||||
@ -34,53 +28,57 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
|
|||||||
size = "small",
|
size = "small",
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false);
|
const [autoComplete, setAutoComplete] = useState<{
|
||||||
const [searchState, sendSearch] = useMachine(searchUserMachine);
|
value: string;
|
||||||
const { searchResults } = searchState.context;
|
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.
|
const { debounced: debouncedInputOnChange } = useDebouncedFunction(
|
||||||
// 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(
|
|
||||||
(event: ChangeEvent<HTMLInputElement>) => {
|
(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
sendSearch("SEARCH", { query: event.target.value });
|
setAutoComplete((state) => ({
|
||||||
|
...state,
|
||||||
|
value: event.target.value,
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
1000,
|
750,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Autocomplete
|
<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}
|
className={className}
|
||||||
options={searchResults ?? []}
|
options={usersQuery.data?.users ?? []}
|
||||||
loading={searchState.matches("searching")}
|
loading={usersQuery.isLoading}
|
||||||
value={value}
|
value={value}
|
||||||
id="user-autocomplete"
|
id="user-autocomplete"
|
||||||
open={isAutocompleteOpen}
|
open={autoComplete.open}
|
||||||
onOpen={() => {
|
onOpen={() => {
|
||||||
setIsAutocompleteOpen(true);
|
setAutoComplete((state) => ({
|
||||||
|
...state,
|
||||||
|
open: true,
|
||||||
|
}));
|
||||||
}}
|
}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setIsAutocompleteOpen(false);
|
setAutoComplete({
|
||||||
|
value: value?.email ?? "",
|
||||||
|
open: false,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
onChange={(_, newValue) => {
|
onChange={(_, newValue) => {
|
||||||
if (newValue === null) {
|
|
||||||
sendSearch("CLEAR_RESULTS");
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option: User, value: User) =>
|
isOptionEqualToValue={(option: User, value: User) =>
|
||||||
@ -97,7 +95,6 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<>
|
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
fullWidth
|
fullWidth
|
||||||
@ -107,7 +104,7 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
|
|||||||
className={styles.textField}
|
className={styles.textField}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
...params.InputProps,
|
...params.InputProps,
|
||||||
onChange: debouncedOnChange,
|
onChange: debouncedInputOnChange,
|
||||||
startAdornment: value && (
|
startAdornment: value && (
|
||||||
<Avatar size="sm" src={value.avatar_url}>
|
<Avatar size="sm" src={value.avatar_url}>
|
||||||
{value.username}
|
{value.username}
|
||||||
@ -115,7 +112,7 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
|
|||||||
),
|
),
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<>
|
<>
|
||||||
{searchState.matches("searching") ? (
|
{usersQuery.isFetching && autoComplete.open ? (
|
||||||
<CircularProgress size={16} />
|
<CircularProgress size={16} />
|
||||||
) : null}
|
) : null}
|
||||||
{params.InputProps.endAdornment}
|
{params.InputProps.endAdornment}
|
||||||
@ -129,7 +126,6 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
|
|||||||
shrink: true,
|
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