mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
Fix issues with admin settings pages UI resetting dropdown menus on unrelated state changes (#16468)
This commit is contained in:
parent
6d1eee6279
commit
94af293ec6
1
changes/14850-fix-ui-settings-action-dropdowns
Normal file
1
changes/14850-fix-ui-settings-action-dropdowns
Normal file
@ -0,0 +1 @@
|
||||
- Fixed UI issues where dropdown menus were not displaying correctly in the administrative settings page.
|
@ -1,6 +1,5 @@
|
||||
import React, { useState, useContext, useCallback } from "react";
|
||||
import React, { useState, useContext, useCallback, useMemo } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import memoize from "memoize-one";
|
||||
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { IConfig } from "interfaces/config";
|
||||
@ -12,22 +11,17 @@ import {
|
||||
IIntegrations,
|
||||
} from "interfaces/integration";
|
||||
import { IApiError } from "interfaces/errors";
|
||||
import { IEmptyTableProps } from "interfaces/empty_table";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
// @ts-ignore
|
||||
|
||||
import configAPI from "services/entities/config";
|
||||
|
||||
import TableContainer from "components/TableContainer";
|
||||
import TableDataError from "components/DataError";
|
||||
import EmptyTable from "components/EmptyTable";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import SectionHeader from "components/SectionHeader";
|
||||
|
||||
import AddIntegrationModal from "./components/AddIntegrationModal";
|
||||
import DeleteIntegrationModal from "./components/DeleteIntegrationModal";
|
||||
import EditIntegrationModal from "./components/EditIntegrationModal";
|
||||
import EmptyIntegrationsTable from "./components/EmptyIntegrationsTable";
|
||||
|
||||
import {
|
||||
generateTableHeaders,
|
||||
@ -91,9 +85,9 @@ const Integrations = (): JSX.Element => {
|
||||
}
|
||||
);
|
||||
|
||||
const combineJiraAndZendesk = memoize(() => {
|
||||
return combineDataSets(jiraIntegrations || [], zendeskIntegrations || []);
|
||||
});
|
||||
// TODO: Cleanup useCallbacks, add missing dependencies, use state setter functions, e.g.,
|
||||
// `setShowAddIntegrationModal((prevState) => !prevState)`, instead of including state
|
||||
// variables as dependencies for toggles, etc.
|
||||
|
||||
const toggleAddIntegrationModal = useCallback(() => {
|
||||
setShowAddIntegrationModal(!showAddIntegrationModal);
|
||||
@ -349,53 +343,30 @@ const Integrations = (): JSX.Element => {
|
||||
[integrationEditing, toggleEditIntegrationModal]
|
||||
);
|
||||
|
||||
const onActionSelection = (
|
||||
action: string,
|
||||
integration: IIntegrationTableData
|
||||
): void => {
|
||||
switch (action) {
|
||||
case "edit":
|
||||
toggleEditIntegrationModal(integration);
|
||||
break;
|
||||
case "delete":
|
||||
toggleDeleteIntegrationModal(integration);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
||||
const onActionSelection = useCallback(
|
||||
(action: string, integration: IIntegrationTableData): void => {
|
||||
switch (action) {
|
||||
case "edit":
|
||||
toggleEditIntegrationModal(integration);
|
||||
break;
|
||||
case "delete":
|
||||
toggleDeleteIntegrationModal(integration);
|
||||
break;
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
},
|
||||
[toggleEditIntegrationModal, toggleDeleteIntegrationModal]
|
||||
);
|
||||
|
||||
const emptyState = () => {
|
||||
const emptyIntegrations: IEmptyTableProps = {
|
||||
graphicName: "empty-integrations",
|
||||
header: "Set up integrations",
|
||||
info:
|
||||
"Create tickets automatically when Fleet detects new software vulnerabilities or hosts failing policies.",
|
||||
additionalInfo: (
|
||||
<>
|
||||
Want to learn more?
|
||||
<CustomLink
|
||||
url="https://fleetdm.com/docs/using-fleet/automations"
|
||||
text="Read about automations"
|
||||
newTab
|
||||
/>
|
||||
</>
|
||||
),
|
||||
primaryButton: (
|
||||
<Button
|
||||
variant="brand"
|
||||
className={`${noIntegrationsClass}__add-button`}
|
||||
onClick={toggleAddIntegrationModal}
|
||||
>
|
||||
Add integration
|
||||
</Button>
|
||||
),
|
||||
};
|
||||
return emptyIntegrations;
|
||||
};
|
||||
const tableHeaders = useMemo(() => generateTableHeaders(onActionSelection), [
|
||||
onActionSelection,
|
||||
]);
|
||||
|
||||
const tableHeaders = generateTableHeaders(onActionSelection);
|
||||
|
||||
const tableData = combineJiraAndZendesk();
|
||||
const tableData = useMemo(
|
||||
() => combineDataSets(jiraIntegrations || [], zendeskIntegrations || []),
|
||||
[jiraIntegrations, zendeskIntegrations]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}`}>
|
||||
@ -421,15 +392,12 @@ const Integrations = (): JSX.Element => {
|
||||
hideButton: !tableData?.length,
|
||||
}}
|
||||
resultsTitle={"integrations"}
|
||||
emptyComponent={() =>
|
||||
EmptyTable({
|
||||
graphicName: emptyState().graphicName,
|
||||
header: emptyState().header,
|
||||
info: emptyState().info,
|
||||
additionalInfo: emptyState().additionalInfo,
|
||||
primaryButton: emptyState().primaryButton,
|
||||
})
|
||||
}
|
||||
emptyComponent={() => (
|
||||
<EmptyIntegrationsTable
|
||||
className={noIntegrationsClass}
|
||||
onActionButtonClick={toggleAddIntegrationModal}
|
||||
/>
|
||||
)}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
disablePagination
|
||||
|
@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import EmptyTable from "components/EmptyTable";
|
||||
import CustomLink from "components/CustomLink";
|
||||
|
||||
const EmptyIntegrationsTable = ({
|
||||
className,
|
||||
onActionButtonClick,
|
||||
}: {
|
||||
className: string;
|
||||
onActionButtonClick: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<EmptyTable
|
||||
graphicName="empty-integrations"
|
||||
header="Set up integrations"
|
||||
info="Create tickets automatically when Fleet detects new software vulnerabilities or hosts failing policies."
|
||||
additionalInfo={
|
||||
<>
|
||||
Want to learn more?
|
||||
<CustomLink
|
||||
url="https://fleetdm.com/docs/using-fleet/automations"
|
||||
text="Read about automations"
|
||||
newTab
|
||||
/>
|
||||
</>
|
||||
}
|
||||
primaryButton={
|
||||
<Button
|
||||
variant="brand"
|
||||
className={`${className}__add-button`}
|
||||
onClick={onActionButtonClick}
|
||||
>
|
||||
Add integration
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyIntegrationsTable;
|
@ -5,7 +5,6 @@ import { Link } from "react-router";
|
||||
import { AppContext } from "context/app";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import useTeamIdParam from "hooks/useTeamIdParam";
|
||||
import { IEmptyTableProps } from "interfaces/empty_table";
|
||||
import { IApiError } from "interfaces/errors";
|
||||
import { INewTeamUsersBody, ITeam } from "interfaces/team";
|
||||
import { IUpdateUserFormData, IUser, IUserFormErrors } from "interfaces/user";
|
||||
@ -16,12 +15,9 @@ import inviteAPI from "services/entities/invites";
|
||||
import teamsAPI, { ILoadTeamsResponse } from "services/entities/teams";
|
||||
import { DEFAULT_CREATE_USER_ERRORS } from "utilities/constants";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import TableContainer from "components/TableContainer";
|
||||
import TableDataError from "components/DataError";
|
||||
import EmptyTable from "components/EmptyTable";
|
||||
import Spinner from "components/Spinner";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import CreateUserModal from "pages/admin/UserManagementPage/components/CreateUserModal";
|
||||
import EditUserModal from "../../../UserManagementPage/components/EditUserModal";
|
||||
import {
|
||||
@ -29,6 +25,7 @@ import {
|
||||
NewUserType,
|
||||
} from "../../../UserManagementPage/components/UserForm/UserForm";
|
||||
import userManagementHelpers from "../../../UserManagementPage/helpers";
|
||||
import EmptyMembersTable from "./components/EmptyUsersTable";
|
||||
import AddUsersModal from "./components/AddUsersModal/AddUsersModal";
|
||||
import RemoveUserModal from "./components/RemoveUserModal/RemoveUserModal";
|
||||
|
||||
@ -354,63 +351,30 @@ const UsersPage = ({ location, router }: ITeamSubnavProps): JSX.Element => {
|
||||
]
|
||||
);
|
||||
|
||||
const onActionSelection = (action: string, user: IUser): void => {
|
||||
switch (action) {
|
||||
case "edit":
|
||||
toggleEditUserModal(user);
|
||||
break;
|
||||
case "remove":
|
||||
toggleRemoveUserModal(user);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
||||
const onActionSelection = useCallback(
|
||||
(action: string, user: IUser): void => {
|
||||
switch (action) {
|
||||
case "edit":
|
||||
toggleEditUserModal(user);
|
||||
break;
|
||||
case "remove":
|
||||
toggleRemoveUserModal(user);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
},
|
||||
[toggleEditUserModal, toggleRemoveUserModal]
|
||||
);
|
||||
|
||||
const emptyState = () => {
|
||||
const emptyUsers: IEmptyTableProps = {
|
||||
graphicName: "empty-users",
|
||||
header: "No users on this team",
|
||||
info: (
|
||||
<>
|
||||
<CustomLink url={PATHS.ADMIN_USERS} text="Global users" /> can still
|
||||
access this team.
|
||||
</>
|
||||
),
|
||||
};
|
||||
if (searchString !== "") {
|
||||
delete emptyUsers.graphicName;
|
||||
emptyUsers.header = "We couldn’t find any users.";
|
||||
emptyUsers.info =
|
||||
"Expecting to see users? Try again in a few seconds as the system catches up.";
|
||||
} else if (isGlobalAdmin) {
|
||||
emptyUsers.primaryButton = (
|
||||
<Button
|
||||
variant="brand"
|
||||
className={`${noUsersClass}__create-button`}
|
||||
onClick={toggleAddUserModal}
|
||||
>
|
||||
Add users
|
||||
</Button>
|
||||
);
|
||||
} else if (isTeamAdmin) {
|
||||
emptyUsers.primaryButton = (
|
||||
<Button
|
||||
variant="brand"
|
||||
className={`${noUsersClass}__create-button`}
|
||||
onClick={toggleCreateUserModal}
|
||||
>
|
||||
Create user
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return emptyUsers;
|
||||
};
|
||||
const columnConfigs = useMemo(
|
||||
() => generateColumnConfigs(onActionSelection),
|
||||
[onActionSelection]
|
||||
);
|
||||
|
||||
if (!isRouteOk) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const columnConfigs = generateColumnConfigs(onActionSelection);
|
||||
const userIds = teamUsers ? teamUsers.map((user) => user.id) : [];
|
||||
|
||||
return (
|
||||
@ -446,14 +410,16 @@ const UsersPage = ({ location, router }: ITeamSubnavProps): JSX.Element => {
|
||||
}}
|
||||
onQueryChange={({ searchQuery }) => setSearchString(searchQuery)}
|
||||
inputPlaceHolder={"Search"}
|
||||
emptyComponent={() =>
|
||||
EmptyTable({
|
||||
graphicName: emptyState().graphicName,
|
||||
header: emptyState().header,
|
||||
info: emptyState().info,
|
||||
primaryButton: emptyState().primaryButton,
|
||||
})
|
||||
}
|
||||
emptyComponent={() => (
|
||||
<EmptyMembersTable
|
||||
className={noUsersClass}
|
||||
isGlobalAdmin={!!isGlobalAdmin}
|
||||
isTeamAdmin={!!isTeamAdmin}
|
||||
searchString={searchString}
|
||||
toggleAddUserModal={toggleAddUserModal}
|
||||
toggleCreateMemberModal={toggleCreateUserModal}
|
||||
/>
|
||||
)}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
searchable={userIds.length > 0 || searchString !== ""}
|
||||
|
@ -0,0 +1,93 @@
|
||||
import React from "react";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import EmptyTable from "components/EmptyTable";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import PATHS from "router/paths";
|
||||
|
||||
interface IEmptyUsersTableProps {
|
||||
className: string;
|
||||
searchString: string;
|
||||
isGlobalAdmin: boolean;
|
||||
isTeamAdmin: boolean;
|
||||
toggleAddUserModal: () => void;
|
||||
toggleCreateMemberModal: () => void;
|
||||
}
|
||||
|
||||
const infoLink = (
|
||||
<>
|
||||
<CustomLink url={PATHS.ADMIN_USERS} text="Global users" /> can still access
|
||||
this team.
|
||||
</>
|
||||
);
|
||||
|
||||
const CreateUserButton = ({
|
||||
className,
|
||||
isGlobalAdmin,
|
||||
isTeamAdmin,
|
||||
toggleAddUserModal,
|
||||
toggleCreateMemberModal,
|
||||
}: Omit<IEmptyUsersTableProps, "searchString">) => {
|
||||
if (!isGlobalAdmin && !isTeamAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isGlobalAdmin) {
|
||||
return (
|
||||
<Button
|
||||
variant="brand"
|
||||
className={`${className}__create-button`}
|
||||
onClick={toggleAddUserModal}
|
||||
>
|
||||
Add user
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="brand"
|
||||
className={`${className}__create-button`}
|
||||
onClick={toggleCreateMemberModal}
|
||||
>
|
||||
Create user
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const EmptyMembersTable = ({
|
||||
className,
|
||||
isGlobalAdmin,
|
||||
isTeamAdmin,
|
||||
searchString,
|
||||
toggleAddUserModal,
|
||||
toggleCreateMemberModal,
|
||||
}: IEmptyUsersTableProps) => {
|
||||
if (searchString !== "") {
|
||||
return (
|
||||
<EmptyTable
|
||||
header="We couldn’t find any users."
|
||||
info="Expecting to see users? Try again in a few seconds as the system catches up."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EmptyTable
|
||||
graphicName="empty-users"
|
||||
header="No users on this team"
|
||||
info={infoLink}
|
||||
primaryButton={
|
||||
<CreateUserButton
|
||||
className={className}
|
||||
isGlobalAdmin={isGlobalAdmin}
|
||||
isTeamAdmin={isTeamAdmin}
|
||||
toggleAddUserModal={toggleAddUserModal}
|
||||
toggleCreateMemberModal={toggleCreateMemberModal}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyMembersTable;
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useCallback, useContext } from "react";
|
||||
import React, { useState, useCallback, useContext, useMemo } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
|
||||
@ -6,24 +6,22 @@ import { NotificationContext } from "context/notification";
|
||||
import { AppContext } from "context/app";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { IApiError } from "interfaces/errors";
|
||||
import { IEmptyTableProps } from "interfaces/empty_table";
|
||||
import usersAPI, { IGetMeResponse } from "services/entities/users";
|
||||
import teamsAPI, {
|
||||
ILoadTeamsResponse,
|
||||
ITeamFormData,
|
||||
} from "services/entities/teams";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import TableContainer from "components/TableContainer";
|
||||
import TableDataError from "components/DataError";
|
||||
import EmptyTable from "components/EmptyTable";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import SandboxGate from "components/Sandbox/SandboxGate";
|
||||
import SandboxMessage from "components/Sandbox/SandboxMessage";
|
||||
|
||||
import CreateTeamModal from "./components/CreateTeamModal";
|
||||
import DeleteTeamModal from "./components/DeleteTeamModal";
|
||||
import EditTeamModal from "./components/EditTeamModal";
|
||||
import EmptyTeamsTable from "./components/EmptyTeamsTable";
|
||||
|
||||
import { generateTableHeaders, generateDataSet } from "./TeamTableConfig";
|
||||
|
||||
const baseClass = "team-management";
|
||||
@ -42,7 +40,6 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
const [showDeleteTeamModal, setShowDeleteTeamModal] = useState(false);
|
||||
const [showEditTeamModal, setShowEditTeamModal] = useState(false);
|
||||
const [teamEditing, setTeamEditing] = useState<ITeam>();
|
||||
const [searchString, setSearchString] = useState("");
|
||||
const [backendValidators, setBackendValidators] = useState<{
|
||||
[key: string]: string;
|
||||
}>({});
|
||||
@ -70,6 +67,10 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
}
|
||||
);
|
||||
|
||||
// TODO: Cleanup useCallbacks, add missing dependencies, use state setter functions, e.g.,
|
||||
// `setShowCreateTeamModal((prevState) => !prevState)`, instead of including state
|
||||
// variables as dependencies for toggles, etc.
|
||||
|
||||
const toggleCreateTeamModal = useCallback(() => {
|
||||
setShowCreateTeamModal(!showCreateTeamModal);
|
||||
setBackendValidators({});
|
||||
@ -97,21 +98,6 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
]
|
||||
);
|
||||
|
||||
const onQueryChange = useCallback(
|
||||
(queryData) => {
|
||||
if (teams) {
|
||||
setSearchString(queryData.searchQuery);
|
||||
const { pageIndex, pageSize, searchQuery } = queryData;
|
||||
teamsAPI.loadAll({
|
||||
page: pageIndex,
|
||||
perPage: pageSize,
|
||||
globalFilter: searchQuery,
|
||||
});
|
||||
}
|
||||
},
|
||||
[setSearchString]
|
||||
);
|
||||
|
||||
const onCreateSubmit = useCallback(
|
||||
(formData: ITeamFormData) => {
|
||||
setIsUpdatingTeams(true);
|
||||
@ -138,7 +124,7 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
setIsUpdatingTeams(false);
|
||||
});
|
||||
},
|
||||
[toggleCreateTeamModal]
|
||||
[toggleCreateTeamModal, refetchMe, refetchTeams, renderFlash]
|
||||
);
|
||||
|
||||
const onDeleteSubmit = useCallback(() => {
|
||||
@ -165,7 +151,15 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
toggleDeleteTeamModal();
|
||||
});
|
||||
}
|
||||
}, [teamEditing, toggleDeleteTeamModal]);
|
||||
}, [
|
||||
currentTeam,
|
||||
teamEditing,
|
||||
refetchMe,
|
||||
refetchTeams,
|
||||
renderFlash,
|
||||
setCurrentTeam,
|
||||
toggleDeleteTeamModal,
|
||||
]);
|
||||
|
||||
const onEditSubmit = useCallback(
|
||||
(formData: ITeamFormData) => {
|
||||
@ -202,54 +196,30 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
});
|
||||
}
|
||||
},
|
||||
[teamEditing, toggleEditTeamModal]
|
||||
[teamEditing, toggleEditTeamModal, refetchTeams, renderFlash]
|
||||
);
|
||||
|
||||
const onActionSelection = (action: string, team: ITeam): void => {
|
||||
switch (action) {
|
||||
case "edit":
|
||||
toggleEditTeamModal(team);
|
||||
break;
|
||||
case "delete":
|
||||
toggleDeleteTeamModal(team);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
||||
const onActionSelection = useCallback(
|
||||
(action: string, team: ITeam): void => {
|
||||
switch (action) {
|
||||
case "edit":
|
||||
toggleEditTeamModal(team);
|
||||
break;
|
||||
case "delete":
|
||||
toggleDeleteTeamModal(team);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
},
|
||||
[toggleEditTeamModal, toggleDeleteTeamModal]
|
||||
);
|
||||
|
||||
const emptyState = () => {
|
||||
const emptyTeams: IEmptyTableProps = {
|
||||
graphicName: "empty-teams",
|
||||
header: "Set up team permissions",
|
||||
info:
|
||||
"Keep your organization organized and efficient by ensuring every user has the correct access to the right hosts.",
|
||||
additionalInfo: (
|
||||
<>
|
||||
{" "}
|
||||
Want to learn more?
|
||||
<CustomLink
|
||||
url="https://fleetdm.com/docs/using-fleet/teams"
|
||||
text="Read about teams"
|
||||
newTab
|
||||
/>
|
||||
</>
|
||||
),
|
||||
primaryButton: (
|
||||
<Button
|
||||
variant="brand"
|
||||
className={`${noTeamsClass}__create-button`}
|
||||
onClick={toggleCreateTeamModal}
|
||||
>
|
||||
Create team
|
||||
</Button>
|
||||
),
|
||||
};
|
||||
|
||||
return emptyTeams;
|
||||
};
|
||||
|
||||
const tableHeaders = generateTableHeaders(onActionSelection);
|
||||
const tableData = teams ? generateDataSet(teams) : [];
|
||||
const tableHeaders = useMemo(() => generateTableHeaders(onActionSelection), [
|
||||
onActionSelection,
|
||||
]);
|
||||
const tableData = useMemo(() => (teams ? generateDataSet(teams) : []), [
|
||||
teams,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}`}>
|
||||
@ -275,28 +245,22 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
isLoading={isFetchingTeams}
|
||||
defaultSortHeader={"name"}
|
||||
defaultSortDirection={"asc"}
|
||||
inputPlaceHolder={"Search"}
|
||||
actionButton={{
|
||||
name: "create team",
|
||||
buttonText: "Create team",
|
||||
variant: "brand",
|
||||
onActionButtonClick: toggleCreateTeamModal,
|
||||
hideButton: teams && teams.length === 0 && searchString === "",
|
||||
hideButton: teams && teams.length === 0,
|
||||
}}
|
||||
onQueryChange={onQueryChange}
|
||||
resultsTitle={"teams"}
|
||||
emptyComponent={() =>
|
||||
EmptyTable({
|
||||
graphicName: "empty-teams",
|
||||
header: emptyState().header,
|
||||
info: emptyState().info,
|
||||
additionalInfo: emptyState().additionalInfo,
|
||||
primaryButton: emptyState().primaryButton,
|
||||
})
|
||||
}
|
||||
emptyComponent={() => (
|
||||
<EmptyTeamsTable
|
||||
className={noTeamsClass}
|
||||
onActionButtonClick={toggleCreateTeamModal}
|
||||
/>
|
||||
)}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
searchable={teams && teams.length > 0 && searchString !== ""}
|
||||
isClientSidePagination
|
||||
/>
|
||||
)}
|
||||
|
@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import EmptyTable from "components/EmptyTable";
|
||||
import CustomLink from "components/CustomLink";
|
||||
|
||||
const EmptyTeamsTable = ({
|
||||
className,
|
||||
onActionButtonClick,
|
||||
}: {
|
||||
className: string;
|
||||
onActionButtonClick: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<EmptyTable
|
||||
graphicName="empty-teams"
|
||||
header="Set up team permissions"
|
||||
info="Keep your organization organized and efficient by ensuring every user has the correct access to the right hosts."
|
||||
additionalInfo={
|
||||
<>
|
||||
{" "}
|
||||
Want to learn more?
|
||||
<CustomLink
|
||||
url="https://fleetdm.com/docs/using-fleet/teams"
|
||||
text="Read about teams"
|
||||
newTab
|
||||
/>
|
||||
</>
|
||||
}
|
||||
primaryButton={
|
||||
<Button
|
||||
variant="brand"
|
||||
className={`${className}__create-button`}
|
||||
onClick={onActionButtonClick}
|
||||
>
|
||||
Create team
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyTeamsTable;
|
@ -28,6 +28,13 @@ import { NewUserType } from "../UserForm/UserForm";
|
||||
import CreateUserModal from "../CreateUserModal";
|
||||
import EditUserModal from "../EditUserModal";
|
||||
|
||||
const EmptyUsersTable = () => (
|
||||
<EmptyTable
|
||||
header="No users match the current criteria."
|
||||
info="Expecting to see users? Try again in a few seconds as the system catches up."
|
||||
/>
|
||||
);
|
||||
|
||||
interface IUsersTableProps {
|
||||
router: InjectedRouter; // v3
|
||||
}
|
||||
@ -94,6 +101,10 @@ const UsersTable = ({ router }: IUsersTableProps): JSX.Element => {
|
||||
}
|
||||
);
|
||||
|
||||
// TODO: Cleanup useCallbacks, add missing dependencies, use state setter functions, e.g.,
|
||||
// `setShowCreateUserModal((prevState) => !prevState)`, instead of including state
|
||||
// variables as dependencies for toggles, etc.
|
||||
|
||||
// TOGGLE MODALS
|
||||
|
||||
const toggleCreateUserModal = useCallback(() => {
|
||||
@ -140,40 +151,54 @@ const UsersTable = ({ router }: IUsersTableProps): JSX.Element => {
|
||||
|
||||
// FUNCTIONS
|
||||
|
||||
const combineUsersAndInvites = useCallback(
|
||||
(usersData, invitesData, currentUserId) => {
|
||||
return combineDataSets(usersData, invitesData, currentUserId);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const goToUserSettingsPage = () => {
|
||||
const goToUserSettingsPage = useCallback(() => {
|
||||
const { USER_SETTINGS } = paths;
|
||||
router.push(USER_SETTINGS);
|
||||
};
|
||||
}, [router]);
|
||||
|
||||
const onActionSelect = (value: string, user: IUser | IInvite) => {
|
||||
switch (value) {
|
||||
case "edit":
|
||||
toggleEditUserModal(user);
|
||||
break;
|
||||
case "delete":
|
||||
toggleDeleteUserModal(user);
|
||||
break;
|
||||
case "passwordReset":
|
||||
toggleResetPasswordUserModal(user);
|
||||
break;
|
||||
case "resetSessions":
|
||||
toggleResetSessionsUserModal(user);
|
||||
break;
|
||||
case "editMyAccount":
|
||||
goToUserSettingsPage();
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const onActionSelect = useCallback(
|
||||
(value: string, user: IUser | IInvite) => {
|
||||
switch (value) {
|
||||
case "edit":
|
||||
toggleEditUserModal(user);
|
||||
break;
|
||||
case "delete":
|
||||
toggleDeleteUserModal(user);
|
||||
break;
|
||||
case "passwordReset":
|
||||
toggleResetPasswordUserModal(user);
|
||||
break;
|
||||
case "resetSessions":
|
||||
toggleResetSessionsUserModal(user);
|
||||
break;
|
||||
case "editMyAccount":
|
||||
goToUserSettingsPage();
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[
|
||||
toggleEditUserModal,
|
||||
toggleDeleteUserModal,
|
||||
toggleResetPasswordUserModal,
|
||||
toggleResetSessionsUserModal,
|
||||
goToUserSettingsPage,
|
||||
]
|
||||
);
|
||||
|
||||
const onTableQueryChange = useCallback(
|
||||
(queryData: ITableQueryData) => {
|
||||
const { searchQuery } = queryData;
|
||||
|
||||
setQuerySearchText(searchQuery);
|
||||
|
||||
refetchUsers();
|
||||
refetchInvites();
|
||||
},
|
||||
[refetchUsers, refetchInvites]
|
||||
);
|
||||
|
||||
const getUser = (type: string, id: number) => {
|
||||
let userData;
|
||||
@ -501,9 +526,9 @@ const UsersTable = ({ router }: IUsersTableProps): JSX.Element => {
|
||||
);
|
||||
};
|
||||
|
||||
const tableHeaders = generateTableHeaders(
|
||||
onActionSelect,
|
||||
isPremiumTier || false
|
||||
const tableHeaders = useMemo(
|
||||
() => generateTableHeaders(onActionSelect, isPremiumTier || false),
|
||||
[onActionSelect, isPremiumTier]
|
||||
);
|
||||
|
||||
const loadingTableData =
|
||||
@ -513,72 +538,42 @@ const UsersTable = ({ router }: IUsersTableProps): JSX.Element => {
|
||||
|
||||
const tableData = useMemo(
|
||||
() =>
|
||||
!loadingTableData && !tableDataError
|
||||
? combineUsersAndInvites(users, invites, currentUser?.id)
|
||||
!loadingTableData &&
|
||||
!tableDataError &&
|
||||
users &&
|
||||
invites &&
|
||||
currentUser?.id
|
||||
? combineDataSets(users, invites, currentUser.id)
|
||||
: [],
|
||||
[
|
||||
loadingTableData,
|
||||
tableDataError,
|
||||
users,
|
||||
invites,
|
||||
currentUser?.id,
|
||||
combineUsersAndInvites,
|
||||
]
|
||||
[loadingTableData, tableDataError, users, invites, currentUser?.id]
|
||||
);
|
||||
|
||||
const renderTable = useCallback(() => {
|
||||
// NOTE: this is called once on the initial rendering. The initial render of
|
||||
// the TableContainer child component calls this handler.
|
||||
const onTableQueryChange = (queryData: ITableQueryData) => {
|
||||
const { searchQuery } = queryData;
|
||||
|
||||
setQuerySearchText(searchQuery);
|
||||
|
||||
refetchUsers();
|
||||
refetchInvites();
|
||||
};
|
||||
|
||||
const emptyState = {
|
||||
header: "No users match the current criteria.",
|
||||
info:
|
||||
"Expecting to see users? Try again in a few seconds as the system catches up.",
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer
|
||||
columnConfigs={tableHeaders}
|
||||
data={tableData}
|
||||
isLoading={loadingTableData}
|
||||
defaultSortHeader={"name"}
|
||||
defaultSortDirection={"asc"}
|
||||
inputPlaceHolder={"Search by name or email"}
|
||||
actionButton={{
|
||||
name: "create user",
|
||||
buttonText: "Create user",
|
||||
onActionButtonClick: toggleCreateUserModal,
|
||||
}}
|
||||
onQueryChange={onTableQueryChange}
|
||||
resultsTitle={"users"}
|
||||
emptyComponent={() => EmptyTable(emptyState)}
|
||||
searchable
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
isClientSidePagination
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
tableHeaders,
|
||||
tableData,
|
||||
loadingTableData,
|
||||
toggleCreateUserModal,
|
||||
refetchUsers,
|
||||
refetchInvites,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* TODO: find a way to move these controls into the table component */}
|
||||
{tableDataError ? <TableDataError /> : renderTable()}
|
||||
{tableDataError ? (
|
||||
<TableDataError />
|
||||
) : (
|
||||
<TableContainer
|
||||
columnConfigs={tableHeaders}
|
||||
data={tableData}
|
||||
isLoading={loadingTableData}
|
||||
defaultSortHeader={"name"}
|
||||
defaultSortDirection={"asc"}
|
||||
inputPlaceHolder={"Search by name or email"}
|
||||
actionButton={{
|
||||
name: "create user",
|
||||
buttonText: "Create user",
|
||||
onActionButtonClick: toggleCreateUserModal,
|
||||
}}
|
||||
onQueryChange={onTableQueryChange}
|
||||
resultsTitle={"users"}
|
||||
emptyComponent={EmptyUsersTable}
|
||||
searchable
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
isClientSidePagination
|
||||
/>
|
||||
)}
|
||||
{showCreateUserModal && renderCreateUserModal()}
|
||||
{showEditUserModal && renderEditUserModal()}
|
||||
{showDeleteUserModal && renderDeleteUserModal()}
|
||||
|
Loading…
Reference in New Issue
Block a user