chore(site): remove search users and groups xservice (#10353)

This commit is contained in:
Bruno Quaresma
2023-10-20 09:33:07 -03:00
committed by GitHub
parent d33526108f
commit 1ba5169109
10 changed files with 97 additions and 204 deletions
+11
View File
@@ -6,6 +6,7 @@ import {
type TemplateVersion,
CreateTemplateRequest,
ProvisionerJob,
UsersRequest,
TemplateRole,
} from "api/typesGenerated";
import {
@@ -156,6 +157,16 @@ export const updateActiveTemplateVersion = (
};
};
export const templaceACLAvailable = (
templateId: string,
options: UsersRequest,
) => {
return {
queryKey: ["template", templateId, "aclAvailable", options],
queryFn: () => API.getTemplateACLAvailable(templateId, options),
};
};
export const templateVersionExternalAuthKey = (versionId: string) => [
"templateVersion",
versionId,
@@ -25,7 +25,7 @@ import { TableRowMenu } from "components/TableRowMenu/TableRowMenu";
import {
UserOrGroupAutocomplete,
UserOrGroupAutocompleteValue,
} from "components/UserOrGroupAutocomplete/UserOrGroupAutocomplete";
} from "./UserOrGroupAutocomplete";
import { type FC, useState } from "react";
import { GroupAvatar } from "components/GroupAvatar/GroupAvatar";
import { getGroupSubtitle } from "utils/groups";
@@ -46,7 +46,6 @@ type AddTemplateUserOrGroupProps = {
const AddTemplateUserOrGroup: React.FC<AddTemplateUserOrGroupProps> = ({
isLoading,
onSubmit,
organizationId,
templateID,
templateACL,
}) => {
@@ -82,7 +81,6 @@ const AddTemplateUserOrGroup: React.FC<AddTemplateUserOrGroupProps> = ({
<Stack direction="row" alignItems="center" spacing={1}>
<UserOrGroupAutocomplete
exclude={excludeFromAutocomplete}
organizationId={organizationId}
templateID={templateID}
value={selectedOption}
onChange={(newValue) => {
@@ -1,83 +1,85 @@
import CircularProgress from "@mui/material/CircularProgress";
import TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete";
import { useMachine } from "@xstate/react";
import Box from "@mui/material/Box";
import { type ChangeEvent, useState } from "react";
import { css } from "@emotion/react";
import type { Group, User } from "api/typesGenerated";
import { AvatarData } from "components/AvatarData/AvatarData";
import { getGroupSubtitle } from "utils/groups";
import { searchUsersAndGroupsMachine } from "xServices/template/searchUsersAndGroupsXService";
import { useDebouncedFunction } from "hooks/debounce";
import { useQuery } from "react-query";
import { templaceACLAvailable } from "api/queries/templates";
import { prepareQuery } from "utils/filters";
export type UserOrGroupAutocompleteValue = User | Group | null;
const isGroup = (value: UserOrGroupAutocompleteValue): value is Group => {
return value !== null && "members" in value;
};
export type UserOrGroupAutocompleteProps = {
value: UserOrGroupAutocompleteValue;
onChange: (value: UserOrGroupAutocompleteValue) => void;
organizationId: string;
templateID?: string;
templateID: string;
exclude: UserOrGroupAutocompleteValue[];
};
const autoCompleteStyles = css`
width: 300px;
& .MuiFormControl-root {
width: 100%;
}
& .MuiInputBase-root {
width: 100%;
}
`;
export const UserOrGroupAutocomplete: React.FC<
UserOrGroupAutocompleteProps
> = ({ value, onChange, organizationId, templateID, exclude }) => {
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false);
const [searchState, sendSearch] = useMachine(searchUsersAndGroupsMachine, {
context: {
userResults: [],
groupResults: [],
organizationId,
templateID,
},
> = ({ value, onChange, templateID, exclude }) => {
const [autoComplete, setAutoComplete] = useState<{
value: string;
open: boolean;
}>({
value: "",
open: false,
});
const { userResults, groupResults } = searchState.context;
const options = [...groupResults, ...userResults].filter((result) => {
const excludeIds = exclude.map((optionToExclude) => optionToExclude?.id);
return !excludeIds.includes(result.id);
const aclAvailableQuery = useQuery({
...templaceACLAvailable(templateID, {
q: prepareQuery(encodeURI(autoComplete.value)),
limit: 25,
}),
enabled: autoComplete.open,
keepPreviousData: true,
});
const options = aclAvailableQuery.data
? [
...aclAvailableQuery.data.groups,
...aclAvailableQuery.data.users,
].filter((result) => {
const excludeIds = exclude.map(
(optionToExclude) => optionToExclude?.id,
);
return !excludeIds.includes(result.id);
})
: [];
const { debounced: handleFilterChange } = useDebouncedFunction(
(event: ChangeEvent<HTMLInputElement>) => {
sendSearch("SEARCH", { query: event.target.value });
setAutoComplete((state) => ({
...state,
value: event.target.value,
}));
},
500,
);
return (
<Autocomplete
noOptionsText="No users or groups found"
value={value}
id="user-or-group-autocomplete"
open={isAutocompleteOpen}
open={autoComplete.open}
onOpen={() => {
setIsAutocompleteOpen(true);
setAutoComplete((state) => ({
...state,
open: true,
}));
}}
onClose={() => {
setIsAutocompleteOpen(false);
setAutoComplete({
value: isGroup(value) ? value.display_name : value?.email ?? "",
open: false,
});
}}
onChange={(_, newValue) => {
if (newValue === null) {
sendSearch("CLEAR_RESULTS");
}
onChange(newValue);
}}
isOptionEqualToValue={(option, value) => option.id === value.id}
@@ -102,7 +104,7 @@ export const UserOrGroupAutocomplete: React.FC<
);
}}
options={options}
loading={searchState.matches("searching")}
loading={aclAvailableQuery.isFetching}
css={autoCompleteStyles}
renderInput={(params) => (
<>
@@ -116,7 +118,7 @@ export const UserOrGroupAutocomplete: React.FC<
onChange: handleFilterChange,
endAdornment: (
<>
{searchState.matches("searching") ? (
{aclAvailableQuery.isFetching ? (
<CircularProgress size={16} />
) : null}
{params.InputProps.endAdornment}
@@ -129,3 +131,19 @@ export const UserOrGroupAutocomplete: React.FC<
/>
);
};
const isGroup = (value: UserOrGroupAutocompleteValue): value is Group => {
return value !== null && "members" in value;
};
const autoCompleteStyles = css`
width: 300px;
& .MuiFormControl-root {
width: 100%;
}
& .MuiInputBase-root {
width: 100%;
}
`;
+5 -1
View File
@@ -11,9 +11,13 @@ import {
} from "components/Filter/filter";
import { BaseOption } from "components/Filter/options";
import { UseFilterMenuOptions, useFilterMenu } from "components/Filter/menu";
import { userFilterQuery } from "utils/filters";
import { docs } from "utils/docs";
const userFilterQuery = {
active: "status:active",
all: "",
};
type StatusOption = BaseOption & {
color: string;
};
@@ -16,9 +16,16 @@ import {
useFilter,
} from "components/Filter/filter";
import { UserFilterMenu, UserMenu } from "components/Filter/UserFilter";
import { workspaceFilterQuery } from "utils/filters";
import { docs } from "utils/docs";
export const workspaceFilterQuery = {
me: "owner:me",
all: "",
running: "status:running",
failed: "status:failed",
dormant: "is-dormant:true",
};
type FilterPreset = {
query: string;
name: string;
+11 -1
View File
@@ -5,7 +5,6 @@ import {
type Health,
} from "api/api";
import { FieldError } from "api/errors";
import { everyOneGroup } from "utils/groups";
import * as TypesGen from "api/typesGenerated";
import range from "lodash/range";
import { Permissions } from "components/AuthProvider/permissions";
@@ -2145,6 +2144,17 @@ export const MockGroup: TypesGen.Group = {
source: "user",
};
const everyOneGroup = (organizationId: string): TypesGen.Group => ({
id: organizationId,
name: "Everyone",
display_name: "",
organization_id: organizationId,
members: [],
avatar_url: "",
quota_allowance: 0,
source: "user",
});
export const MockTemplateACL: TypesGen.TemplateACL = {
group: [
{ ...everyOneGroup(MockOrganization.id), role: "use" },
-20
View File
@@ -1,20 +0,0 @@
import * as TypesGen from "api/typesGenerated";
import { queryToFilter } from "./filters";
describe("queryToFilter", () => {
it.each<
[string | undefined, TypesGen.WorkspaceFilter | TypesGen.UsersRequest]
>([
[undefined, {}],
["", { q: "" }],
["asdkfvjn", { q: "asdkfvjn" }],
["owner:me", { q: "owner:me" }],
["owner:me owner:me2", { q: "owner:me owner:me2" }],
["me/dev", { q: "me/dev" }],
["me/", { q: "me/" }],
[" key:val owner:me ", { q: "key:val owner:me" }],
["status:failed", { q: "status:failed" }],
])(`query=%p, filter=%p`, (query, filter) => {
expect(queryToFilter(query)).toEqual(filter);
});
});
-23
View File
@@ -1,26 +1,3 @@
import * as TypesGen from "api/typesGenerated";
export const queryToFilter = (
query?: string,
): TypesGen.WorkspaceFilter | TypesGen.UsersRequest => {
return {
q: prepareQuery(query),
};
};
export const prepareQuery = (query?: string) => {
return query?.trim().replace(/ +/g, " ");
};
export const workspaceFilterQuery = {
me: "owner:me",
all: "",
running: "status:running",
failed: "status:failed",
dormant: "is-dormant:true",
};
export const userFilterQuery = {
active: "status:active",
all: "",
};
-11
View File
@@ -1,16 +1,5 @@
import { Group } from "api/typesGenerated";
export const everyOneGroup = (organizationId: string): Group => ({
id: organizationId,
name: "Everyone",
display_name: "",
organization_id: organizationId,
members: [],
avatar_url: "",
quota_allowance: 0,
source: "user",
});
/**
* Returns true if the provided group is the 'Everyone' group.
* The everyone group represents all the users in an organization
@@ -1,101 +0,0 @@
import { getGroups, getTemplateACLAvailable, getUsers } from "api/api";
import { Group, User } from "api/typesGenerated";
import { queryToFilter } from "utils/filters";
import { everyOneGroup } from "utils/groups";
import { assign, createMachine } from "xstate";
export type SearchUsersAndGroupsEvent =
| { type: "SEARCH"; query: string }
| { type: "CLEAR_RESULTS" };
export const searchUsersAndGroupsMachine = createMachine(
{
id: "searchUsersAndGroups",
predictableActionArguments: true,
schema: {
context: {} as {
organizationId: string;
templateID?: string;
userResults: User[];
groupResults: Group[];
},
events: {} as SearchUsersAndGroupsEvent,
services: {} as {
search: {
data: {
users: User[];
groups: Group[];
};
};
},
},
tsTypes: {} as import("./searchUsersAndGroupsXService.typegen").Typegen0,
initial: "idle",
states: {
idle: {
on: {
SEARCH: {
target: "searching",
cond: "queryHasMinLength",
},
CLEAR_RESULTS: {
actions: ["clearResults"],
target: "idle",
},
},
},
searching: {
invoke: {
src: "search",
onDone: {
target: "idle",
actions: ["assignSearchResults"],
},
},
},
},
},
{
services: {
search: async ({ organizationId, templateID }, { query }) => {
let users, groups;
if (templateID && templateID !== "") {
const res = await getTemplateACLAvailable(
templateID,
queryToFilter(query),
);
users = res.users;
groups = res.groups;
} else {
const [userRes, groupsRes] = await Promise.all([
getUsers(queryToFilter(query)),
getGroups(organizationId),
]);
users = userRes.users;
groups = groupsRes;
}
// The Everyone groups is not returned by the API so we have to add it
// manually
return {
users: users,
groups: [everyOneGroup(organizationId), ...groups],
};
},
},
actions: {
assignSearchResults: assign({
userResults: (_, { data }) => data.users,
groupResults: (_, { data }) => data.groups,
}),
clearResults: assign({
userResults: (_) => [],
groupResults: (_) => [],
}),
},
guards: {
queryHasMinLength: (_, { query }) => query.length >= 3,
},
},
);