mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
Add UI for team admin role (#2637)
This commit is contained in:
parent
3136cc105e
commit
b12c7ab925
@ -1 +1,2 @@
|
||||
* Add Team Admin role.
|
||||
* Provide UI for Team Admin team management.
|
||||
|
@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { push } from "react-router-redux";
|
||||
|
||||
import { IUser } from "interfaces/user";
|
||||
import permissionUtils from "utilities/permissions";
|
||||
import paths from "router/paths";
|
||||
// @ts-ignore
|
||||
import { renderFlash } from "redux/nodes/notifications/actions";
|
||||
|
||||
interface IAuthAnyAdminRoutesProps {
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
interface IRootState {
|
||||
auth: {
|
||||
user: IUser;
|
||||
};
|
||||
}
|
||||
|
||||
const { HOME } = paths;
|
||||
|
||||
/**
|
||||
* Checks if a user is any admin when routing
|
||||
*/
|
||||
const AuthAnyAdminRoutes = ({
|
||||
children,
|
||||
}: IAuthAnyAdminRoutesProps): JSX.Element | null => {
|
||||
const dispatch = useDispatch();
|
||||
const user = useSelector((state: IRootState) => state.auth.user);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
!permissionUtils.isGlobalAdmin(user) &&
|
||||
!permissionUtils.isAnyTeamAdmin(user)
|
||||
) {
|
||||
dispatch(push(HOME));
|
||||
dispatch(renderFlash("error", "You do not have permissions for that page"));
|
||||
return null;
|
||||
}
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default AuthAnyAdminRoutes;
|
1
frontend/components/AuthAnyAdminRoutes/index.ts
Normal file
1
frontend/components/AuthAnyAdminRoutes/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from "./AuthAnyAdminRoutes";
|
@ -8,7 +8,7 @@ import paths from "router/paths";
|
||||
// @ts-ignore
|
||||
import { renderFlash } from "redux/nodes/notifications/actions";
|
||||
|
||||
interface IAuthAnyMaintainerGlobalAdminRoutesProps {
|
||||
interface IAuthAnyMaintainerAnyAdminRoutesProps {
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
@ -21,11 +21,11 @@ interface IRootState {
|
||||
const { HOME } = paths;
|
||||
|
||||
/**
|
||||
* Checks if a user is a global admin or global maintainer when routing
|
||||
* Checks if a user is any maintainer or any admin when routing
|
||||
*/
|
||||
const AuthAnyMaintainerGlobalAdminRoutes = ({
|
||||
const AuthAnyMaintainerAnyAdminRoutes = ({
|
||||
children,
|
||||
}: IAuthAnyMaintainerGlobalAdminRoutesProps): JSX.Element | null => {
|
||||
}: IAuthAnyMaintainerAnyAdminRoutesProps): JSX.Element | null => {
|
||||
const dispatch = useDispatch();
|
||||
const user = useSelector((state: IRootState) => state.auth.user);
|
||||
|
||||
@ -36,6 +36,7 @@ const AuthAnyMaintainerGlobalAdminRoutes = ({
|
||||
if (
|
||||
!permissionUtils.isGlobalAdmin(user) &&
|
||||
!permissionUtils.isGlobalMaintainer(user) &&
|
||||
!permissionUtils.isAnyTeamAdmin(user) &&
|
||||
!permissionUtils.isAnyTeamMaintainer(user)
|
||||
) {
|
||||
dispatch(push(HOME));
|
||||
@ -45,4 +46,4 @@ const AuthAnyMaintainerGlobalAdminRoutes = ({
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default AuthAnyMaintainerGlobalAdminRoutes;
|
||||
export default AuthAnyMaintainerAnyAdminRoutes;
|
@ -0,0 +1 @@
|
||||
export { default } from "./AuthAnyMaintainerAnyAdminRoutes";
|
@ -1 +0,0 @@
|
||||
export { default } from "./AuthAnyMaintainerGlobalAdminRoutes";
|
@ -14,7 +14,10 @@ interface ITeamsDropdownProps {
|
||||
isLoading: boolean;
|
||||
teams: ITeam[];
|
||||
currentTeamId: number;
|
||||
hideAllTeamsOption?: boolean;
|
||||
onChange: (newSelectedValue: number) => void;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const baseClass = "component__team-dropdown";
|
||||
@ -23,7 +26,10 @@ const TeamsDropdown = ({
|
||||
isLoading,
|
||||
teams,
|
||||
currentTeamId,
|
||||
hideAllTeamsOption = false,
|
||||
onChange,
|
||||
onOpen,
|
||||
onClose,
|
||||
}: ITeamsDropdownProps) => {
|
||||
const { currentUser, isPremiumTier, isOnGlobalTeam } = useContext(AppContext);
|
||||
|
||||
@ -36,7 +42,8 @@ const TeamsDropdown = ({
|
||||
const teamOptions = generateTeamFilterDropdownOptions(
|
||||
teams,
|
||||
currentUser,
|
||||
isOnGlobalTeam as boolean
|
||||
isOnGlobalTeam as boolean,
|
||||
hideAllTeamsOption
|
||||
);
|
||||
const selectedTeamId = getValidatedTeamId(
|
||||
teams,
|
||||
@ -54,6 +61,8 @@ const TeamsDropdown = ({
|
||||
options={teamOptions}
|
||||
searchable={false}
|
||||
onChange={onChange}
|
||||
onOpen={onOpen}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -21,6 +21,8 @@ class Dropdown extends Component {
|
||||
multi: PropTypes.bool,
|
||||
name: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onOpen: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
options: PropTypes.arrayOf(dropdownOptionInterface).isRequired,
|
||||
placeholder: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
|
||||
value: PropTypes.oneOfType([
|
||||
@ -33,6 +35,8 @@ class Dropdown extends Component {
|
||||
|
||||
static defaultProps = {
|
||||
onChange: noop,
|
||||
onOpen: noop,
|
||||
onClose: noop,
|
||||
clearable: false,
|
||||
searchable: true,
|
||||
disabled: false,
|
||||
@ -41,6 +45,16 @@ class Dropdown extends Component {
|
||||
placeholder: "Select One...",
|
||||
};
|
||||
|
||||
onMenuOpen = () => {
|
||||
const { onOpen } = this.props;
|
||||
onOpen();
|
||||
};
|
||||
|
||||
onMenuClose = () => {
|
||||
const { onClose } = this.props;
|
||||
onClose();
|
||||
};
|
||||
|
||||
handleChange = (selected) => {
|
||||
const { multi, onChange, clearable } = this.props;
|
||||
|
||||
@ -84,7 +98,7 @@ class Dropdown extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { handleChange, renderOption } = this;
|
||||
const { handleChange, renderOption, onMenuOpen, onMenuClose } = this;
|
||||
const {
|
||||
error,
|
||||
className,
|
||||
@ -122,6 +136,8 @@ class Dropdown extends Component {
|
||||
optionRenderer={renderOption}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onOpen={onMenuOpen}
|
||||
onClose={onMenuClose}
|
||||
/>
|
||||
</FormField>
|
||||
);
|
||||
|
@ -3,18 +3,6 @@ import URL_PREFIX from "router/url_prefix";
|
||||
import permissionUtils from "utilities/permissions";
|
||||
|
||||
export default (currentUser) => {
|
||||
const adminNavItems = [
|
||||
{
|
||||
icon: "settings",
|
||||
name: "Settings",
|
||||
iconName: "settings",
|
||||
location: {
|
||||
regex: new RegExp(`^${URL_PREFIX}/settings/`),
|
||||
pathname: PATHS.ADMIN_SETTINGS,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const userNavItems = [
|
||||
{
|
||||
icon: "logo",
|
||||
@ -69,7 +57,24 @@ export default (currentUser) => {
|
||||
},
|
||||
];
|
||||
|
||||
if (permissionUtils.isGlobalAdmin(currentUser)) {
|
||||
if (
|
||||
permissionUtils.isAnyTeamAdmin(currentUser) ||
|
||||
permissionUtils.isGlobalAdmin(currentUser)
|
||||
) {
|
||||
const adminNavItems = [
|
||||
{
|
||||
icon: "settings",
|
||||
name: "Settings",
|
||||
iconName: "settings",
|
||||
location: {
|
||||
regex: new RegExp(`^${URL_PREFIX}/settings/`),
|
||||
pathname:
|
||||
currentUser.global_role === "admin"
|
||||
? PATHS.ADMIN_SETTINGS
|
||||
: `${PATHS.ADMIN_TEAMS}/${currentUser.teams[0].id}/members`,
|
||||
},
|
||||
},
|
||||
];
|
||||
return [
|
||||
...userNavItems,
|
||||
...teamMaintainerNavItems,
|
||||
|
@ -23,6 +23,8 @@ type InitialStateType = {
|
||||
isOnGlobalTeam: boolean | undefined;
|
||||
isAnyTeamMaintainer: boolean | undefined;
|
||||
isTeamMaintainer: boolean | undefined;
|
||||
isAnyTeamAdmin: boolean | undefined;
|
||||
isTeamAdmin: boolean | undefined;
|
||||
isOnlyObserver: boolean | undefined;
|
||||
setCurrentUser: (user: IUser) => void;
|
||||
setCurrentTeam: (team: ITeam | undefined) => void;
|
||||
@ -43,6 +45,8 @@ const initialState = {
|
||||
isOnGlobalTeam: undefined,
|
||||
isAnyTeamMaintainer: undefined,
|
||||
isTeamMaintainer: undefined,
|
||||
isAnyTeamAdmin: undefined,
|
||||
isTeamAdmin: undefined,
|
||||
isOnlyObserver: undefined,
|
||||
setCurrentUser: () => null,
|
||||
setCurrentTeam: () => null,
|
||||
@ -129,6 +133,8 @@ const AppProvider = ({ children }: Props) => {
|
||||
isOnGlobalTeam: state.isOnGlobalTeam,
|
||||
isAnyTeamMaintainer: state.isAnyTeamMaintainer,
|
||||
isTeamMaintainer: state.isTeamMaintainer,
|
||||
isTeamAdmin: state.isTeamAdmin,
|
||||
isAnyTeamAdmin: state.isAnyTeamAdmin,
|
||||
isOnlyObserver: state.isOnlyObserver,
|
||||
setCurrentUser: (currentUser: IUser) => {
|
||||
dispatch({ type: actions.SET_CURRENT_USER, currentUser });
|
||||
|
@ -689,7 +689,8 @@ const getSortedTeamOptions = memoize((teams: ITeam[]) =>
|
||||
export const generateTeamFilterDropdownOptions = (
|
||||
teams: ITeam[],
|
||||
currentUser: IUser | null,
|
||||
isOnGlobalTeam: boolean
|
||||
isOnGlobalTeam: boolean,
|
||||
hideAllTeamsOption: boolean
|
||||
) => {
|
||||
let currentUserTeams: ITeam[] = [];
|
||||
if (isOnGlobalTeam) {
|
||||
@ -698,7 +699,7 @@ export const generateTeamFilterDropdownOptions = (
|
||||
currentUserTeams = currentUser.teams;
|
||||
}
|
||||
|
||||
const allTeamsOption = [
|
||||
const allTeamOption = [
|
||||
{
|
||||
disabled: false,
|
||||
label: "All teams",
|
||||
@ -708,7 +709,9 @@ export const generateTeamFilterDropdownOptions = (
|
||||
|
||||
const sortedCurrentUserTeamOptions = getSortedTeamOptions(currentUserTeams);
|
||||
|
||||
return allTeamsOption.concat(sortedCurrentUserTeamOptions);
|
||||
return !hideAllTeamsOption
|
||||
? allTeamOption.concat(sortedCurrentUserTeamOptions)
|
||||
: sortedCurrentUserTeamOptions;
|
||||
};
|
||||
|
||||
export const getValidatedTeamId = (
|
||||
|
13
frontend/interfaces/role.ts
Normal file
13
frontend/interfaces/role.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
export default PropTypes.shape({
|
||||
disabled: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
});
|
||||
|
||||
export interface IRole {
|
||||
disabled: boolean;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
@ -7,6 +7,7 @@ export default PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
name: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
role: PropTypes.string,
|
||||
force_password_reset: PropTypes.bool,
|
||||
gravatar_url: PropTypes.string,
|
||||
sso_enabled: PropTypes.bool,
|
||||
@ -21,6 +22,7 @@ export interface IUser {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
force_password_reset: boolean;
|
||||
gravatar_url: string;
|
||||
sso_enabled: boolean;
|
||||
@ -39,3 +41,10 @@ export interface IUserUpdateBody {
|
||||
email?: string;
|
||||
sso_enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface IUserFormErrors {
|
||||
email: string | null;
|
||||
name: string | null;
|
||||
password: string | null;
|
||||
sso_enabled: boolean | null;
|
||||
}
|
||||
|
@ -1,24 +1,31 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import React, { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
// @ts-ignore
|
||||
import memoize from "memoize-one";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import { IConfig } from "interfaces/config";
|
||||
import { IUser } from "interfaces/user";
|
||||
import { INewMembersBody, ITeam } from "interfaces/team";
|
||||
import { Link } from "react-router";
|
||||
import { AppContext } from "context/app";
|
||||
// ignore TS error for now until these are rewritten in ts.
|
||||
// @ts-ignore
|
||||
import { renderFlash } from "redux/nodes/notifications/actions";
|
||||
// @ts-ignore
|
||||
import userActions from "redux/nodes/entities/users/actions";
|
||||
import teamActions from "redux/nodes/entities/teams/actions";
|
||||
// @ts-ignore
|
||||
import inviteActions from "redux/nodes/entities/invites/actions";
|
||||
import Button from "components/buttons/Button";
|
||||
import TableContainer from "components/TableContainer";
|
||||
import TableDataError from "components/TableDataError";
|
||||
import PATHS from "router/paths";
|
||||
import CreateUserModal from "pages/admin/UserManagementPage/components/CreateUserModal";
|
||||
import { DEFAULT_CREATE_USER_ERRORS } from "utilities/constants";
|
||||
import EditUserModal from "../../../UserManagementPage/components/EditUserModal";
|
||||
import { IFormData } from "../../../UserManagementPage/components/UserForm/UserForm";
|
||||
import {
|
||||
IFormData,
|
||||
NewUserType,
|
||||
} from "../../../UserManagementPage/components/UserForm/UserForm";
|
||||
import userManagementHelpers from "../../../UserManagementPage/helpers";
|
||||
import AddMemberModal from "./components/AddMemberModal";
|
||||
import RemoveMemberModal from "./components/RemoveMemberModal";
|
||||
@ -78,12 +85,15 @@ const MembersPage = ({
|
||||
const teamId = parseInt(team_id, 10);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { isGlobalAdmin, currentUser } = useContext(AppContext);
|
||||
|
||||
const isPremiumTier = useSelector((state: IRootState) => {
|
||||
return state.app.config.tier === "premium";
|
||||
});
|
||||
const loadingTableData = useSelector(
|
||||
(state: IRootState) => state.entities.users.loading
|
||||
);
|
||||
|
||||
const users = useSelector((state: IRootState) =>
|
||||
generateDataSet(teamId, state.entities.users.data)
|
||||
);
|
||||
@ -110,11 +120,19 @@ const MembersPage = ({
|
||||
return state.app.config.enable_sso;
|
||||
});
|
||||
|
||||
const config = useSelector((state: IRootState) => {
|
||||
return state.app.config;
|
||||
});
|
||||
|
||||
const [showAddMemberModal, setShowAddMemberModal] = useState(false);
|
||||
const [showRemoveMemberModal, setShowRemoveMemberModal] = useState(false);
|
||||
const [showEditUserModal, setShowEditUserModal] = useState(false);
|
||||
const [showCreateUserModal, setShowCreateUserModal] = useState(false);
|
||||
const [isFormSubmitting, setIsFormSubmitting] = useState(false);
|
||||
const [userEditing, setUserEditing] = useState<IUser>();
|
||||
const [searchString, setSearchString] = useState<string>("");
|
||||
const [createUserErrors] = useState(DEFAULT_CREATE_USER_ERRORS);
|
||||
const [editUserErrors] = useState(DEFAULT_CREATE_USER_ERRORS);
|
||||
|
||||
const toggleAddUserModal = useCallback(() => {
|
||||
setShowAddMemberModal(!showAddMemberModal);
|
||||
@ -136,6 +154,18 @@ const MembersPage = ({
|
||||
[showEditUserModal, setShowEditUserModal, setUserEditing]
|
||||
);
|
||||
|
||||
const toggleCreateMemberModal = useCallback(() => {
|
||||
setShowCreateUserModal(!showCreateUserModal);
|
||||
setShowAddMemberModal(false);
|
||||
currentUser ? setUserEditing(currentUser) : setUserEditing(undefined);
|
||||
}, [
|
||||
showCreateUserModal,
|
||||
currentUser,
|
||||
setShowCreateUserModal,
|
||||
setUserEditing,
|
||||
setShowAddMemberModal,
|
||||
]);
|
||||
|
||||
const onRemoveMemberSubmit = useCallback(() => {
|
||||
const removedUsers = { users: [{ id: userEditing?.id }] };
|
||||
dispatch(teamActions.removeMembers(teamId, removedUsers))
|
||||
@ -194,6 +224,88 @@ const MembersPage = ({
|
||||
[dispatch, teamId]
|
||||
);
|
||||
|
||||
const onCreateMemberSubmit = (formData: IFormData) => {
|
||||
setIsFormSubmitting(true);
|
||||
|
||||
if (formData.newUserType === NewUserType.AdminInvited) {
|
||||
// Do some data formatting adding `invited_by` for the request to be correct and deleteing uncessary fields
|
||||
const requestData = {
|
||||
...formData,
|
||||
invited_by: formData.currentUserId,
|
||||
};
|
||||
delete requestData.currentUserId; // this field is not needed for the request
|
||||
delete requestData.newUserType; // this field is not needed for the request
|
||||
delete requestData.password; // this field is not needed for the request
|
||||
dispatch(inviteActions.create(requestData))
|
||||
.then(() => {
|
||||
dispatch(
|
||||
renderFlash(
|
||||
"success",
|
||||
`An invitation email was sent from ${config.sender_address} to ${formData.email}.`
|
||||
)
|
||||
);
|
||||
fetchUsers(tableQueryData);
|
||||
toggleCreateMemberModal();
|
||||
})
|
||||
.catch((userErrors: any) => {
|
||||
if (userErrors.base.includes("Duplicate")) {
|
||||
dispatch(
|
||||
renderFlash(
|
||||
"error",
|
||||
"A user with this email address already exists."
|
||||
)
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
renderFlash("error", "Could not create user. Please try again.")
|
||||
);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsFormSubmitting(false);
|
||||
});
|
||||
} else {
|
||||
// Do some data formatting deleteing uncessary fields
|
||||
const requestData = {
|
||||
...formData,
|
||||
};
|
||||
delete requestData.currentUserId; // this field is not needed for the request
|
||||
delete requestData.newUserType; // this field is not needed for the request
|
||||
dispatch(userActions.createUserWithoutInvitation(requestData))
|
||||
.then(() => {
|
||||
dispatch(
|
||||
renderFlash("success", `Successfully created ${requestData.name}.`)
|
||||
);
|
||||
fetchUsers(tableQueryData);
|
||||
toggleCreateMemberModal();
|
||||
})
|
||||
.catch((userErrors: any) => {
|
||||
if (userErrors.base.includes("Duplicate")) {
|
||||
dispatch(
|
||||
renderFlash(
|
||||
"error",
|
||||
"A user with this email address already exists."
|
||||
)
|
||||
);
|
||||
} else if (userErrors.base.includes("already invited")) {
|
||||
dispatch(
|
||||
renderFlash(
|
||||
"error",
|
||||
"A user with this email address has already been invited."
|
||||
)
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
renderFlash("error", "Could not create user. Please try again.")
|
||||
);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsFormSubmitting(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onEditMemberSubmit = useCallback(
|
||||
(formData: IFormData) => {
|
||||
const updatedAttrs = userManagementHelpers.generateUpdateData(
|
||||
@ -201,6 +313,8 @@ const MembersPage = ({
|
||||
formData
|
||||
);
|
||||
|
||||
setIsFormSubmitting(true);
|
||||
|
||||
const userName = userEditing?.name;
|
||||
dispatch(userActions.update(userEditing, updatedAttrs))
|
||||
.then(() => {
|
||||
@ -214,12 +328,19 @@ const MembersPage = ({
|
||||
`Could not edit ${userName}. Please try again.`
|
||||
)
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsFormSubmitting(false);
|
||||
});
|
||||
toggleEditMemberModal();
|
||||
},
|
||||
[dispatch, toggleEditMemberModal, userEditing, fetchUsers]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers(tableQueryData);
|
||||
}, [team_id]);
|
||||
|
||||
// NOTE: this will fire on initial render, so we use this to get the list of
|
||||
// users for this team, as well as use it as a handler when the table query
|
||||
// changes.
|
||||
@ -277,7 +398,7 @@ const MembersPage = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [searchString]);
|
||||
}, [searchString, toggleAddUserModal]);
|
||||
|
||||
const tableHeaders = generateTableHeaders(onActionSelection);
|
||||
|
||||
@ -285,7 +406,11 @@ const MembersPage = ({
|
||||
<div className={baseClass}>
|
||||
<p className={`${baseClass}__page-description`}>
|
||||
Users can either be a member of team(s) or a global user.{" "}
|
||||
<Link to={PATHS.ADMIN_USERS}>Manage users with global access here</Link>
|
||||
{isGlobalAdmin && (
|
||||
<Link to={PATHS.ADMIN_USERS}>
|
||||
Manage users with global access here
|
||||
</Link>
|
||||
)}
|
||||
</p>
|
||||
{Object.keys(usersError).length > 0 ? (
|
||||
<TableDataError />
|
||||
@ -315,21 +440,43 @@ const MembersPage = ({
|
||||
disabledMembers={memberIds}
|
||||
onCancel={toggleAddUserModal}
|
||||
onSubmit={onAddMemberSubmit}
|
||||
onCreateNewMember={toggleCreateMemberModal}
|
||||
/>
|
||||
) : null}
|
||||
{showEditUserModal ? (
|
||||
<EditUserModal
|
||||
serverErrors={editUserErrors}
|
||||
onCancel={toggleEditMemberModal}
|
||||
onSubmit={onEditMemberSubmit}
|
||||
defaultName={userEditing?.name}
|
||||
defaultEmail={userEditing?.email}
|
||||
defaultGlobalRole={userEditing?.global_role}
|
||||
defaultTeamRole={userEditing?.role}
|
||||
defaultTeams={userEditing?.teams}
|
||||
availableTeams={teams}
|
||||
isPremiumTier={isPremiumTier}
|
||||
smtpConfigured={smtpConfigured}
|
||||
canUseSso={canUseSso}
|
||||
isSsoEnabled={userEditing?.sso_enabled}
|
||||
isModifiedByGlobalAdmin={isGlobalAdmin}
|
||||
currentTeam={team}
|
||||
/>
|
||||
) : null}
|
||||
{showCreateUserModal ? (
|
||||
<CreateUserModal
|
||||
serverErrors={createUserErrors}
|
||||
onCancel={toggleCreateMemberModal}
|
||||
onSubmit={onCreateMemberSubmit}
|
||||
defaultGlobalRole={userEditing?.global_role}
|
||||
defaultTeamRole={userEditing?.role}
|
||||
defaultTeams={userEditing?.teams}
|
||||
availableTeams={teams}
|
||||
isPremiumTier={isPremiumTier}
|
||||
smtpConfigured={smtpConfigured}
|
||||
canUseSso={canUseSso}
|
||||
currentTeam={team}
|
||||
isModifiedByGlobalAdmin={isGlobalAdmin}
|
||||
isFormSubmitting={isFormSubmitting}
|
||||
/>
|
||||
) : null}
|
||||
{showRemoveMemberModal ? (
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
|
||||
import { INewMembersBody, ITeam } from "interfaces/team";
|
||||
import { IUser } from "interfaces/user";
|
||||
import endpoints from "fleet/endpoints";
|
||||
import Modal from "components/modals/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
@ -14,12 +15,14 @@ interface IAddMemberModal {
|
||||
disabledMembers: number[];
|
||||
onCancel: () => void;
|
||||
onSubmit: (userIds: INewMembersBody) => void;
|
||||
onCreateNewMember: () => void;
|
||||
}
|
||||
|
||||
const AddMemberModal = ({
|
||||
disabledMembers,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
onCreateNewMember,
|
||||
team,
|
||||
}: IAddMemberModal): JSX.Element => {
|
||||
const [selectedMembers, setSelectedMembers] = useState([]);
|
||||
@ -41,6 +44,7 @@ const AddMemberModal = ({
|
||||
return (
|
||||
<Modal onExit={onCancel} title={"Add Members"} className={baseClass}>
|
||||
<form className={`${baseClass}__form`}>
|
||||
<p className="title">Add team members</p>
|
||||
<AutocompleteDropdown
|
||||
team={team}
|
||||
id={"member-autocomplete"}
|
||||
@ -50,6 +54,18 @@ const AddMemberModal = ({
|
||||
disabledOptions={disabledMembers}
|
||||
value={selectedMembers}
|
||||
/>
|
||||
<p>
|
||||
User not here?
|
||||
<Button
|
||||
onClick={onCreateNewMember}
|
||||
variant={"text-link"}
|
||||
className={"light-text"}
|
||||
>
|
||||
<>
|
||||
<strong>Create a user</strong>
|
||||
</>
|
||||
</Button>
|
||||
</p>
|
||||
<div className={`${baseClass}__btn-wrap`}>
|
||||
<Button
|
||||
disabled={selectedMembers.length === 0}
|
||||
|
@ -1,4 +1,9 @@
|
||||
.add-member-modal {
|
||||
.title {
|
||||
font-weight: $bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&__btn-wrap {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
@ -1,11 +1,15 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import React, { useState, useEffect, useCallback, useContext } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { Link } from "react-router";
|
||||
import { InjectedRouter, Link, RouteProps } from "react-router";
|
||||
import { push } from "react-router-redux";
|
||||
import { Tab, TabList, Tabs } from "react-tabs";
|
||||
import { find, memoize, toNumber } from "lodash";
|
||||
import classnames from "classnames";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { IUser } from "interfaces/user";
|
||||
import { AppContext } from "context/app";
|
||||
// ignore TS error for now until these are rewritten in ts.
|
||||
// @ts-ignore
|
||||
import { renderFlash } from "redux/nodes/notifications/actions";
|
||||
@ -13,6 +17,8 @@ import teamActions from "redux/nodes/entities/teams/actions";
|
||||
import Spinner from "components/loaders/Spinner";
|
||||
import Button from "components/buttons/Button";
|
||||
import TabsWrapper from "components/TabsWrapper";
|
||||
import TeamsDropdown from "components/TeamsDropdown";
|
||||
import { getNextLocationPath } from "pages/admin/UserManagementPage/helpers/userManagementHelpers";
|
||||
import DeleteTeamModal from "../components/DeleteTeamModal";
|
||||
import EditTeamModal from "../components/EditTeamModal";
|
||||
import { IEditTeamFormData } from "../components/EditTeamModal/EditTeamModal";
|
||||
@ -41,6 +47,9 @@ const teamDetailsSubNav: ITeamDetailsSubNavItem[] = [
|
||||
];
|
||||
|
||||
interface IRootState {
|
||||
auth: {
|
||||
user: IUser;
|
||||
};
|
||||
entities: {
|
||||
teams: {
|
||||
loading: boolean;
|
||||
@ -57,8 +66,18 @@ interface ITeamDetailsPageProps {
|
||||
location: {
|
||||
pathname: string;
|
||||
};
|
||||
route: RouteProps;
|
||||
router: InjectedRouter;
|
||||
}
|
||||
|
||||
const getTeams = (data: { [id: string]: ITeam }) => {
|
||||
return Object.keys(data).map((teamId) => {
|
||||
return data[teamId];
|
||||
});
|
||||
};
|
||||
|
||||
const memoizedGetTeams = memoize(getTeams);
|
||||
|
||||
const generateUpdateData = (
|
||||
currentTeamData: ITeam,
|
||||
formData: IEditTeamFormData
|
||||
@ -78,16 +97,27 @@ const getTabIndex = (path: string, teamId: number): number => {
|
||||
};
|
||||
|
||||
const TeamDetailsWrapper = ({
|
||||
route,
|
||||
router,
|
||||
children,
|
||||
location: { pathname },
|
||||
params: { team_id },
|
||||
params: routeParams,
|
||||
}: ITeamDetailsPageProps): JSX.Element => {
|
||||
const { isGlobalAdmin, setCurrentTeam } = useContext(AppContext);
|
||||
|
||||
const isLoadingTeams = useSelector(
|
||||
(state: IRootState) => state.entities.teams.loading
|
||||
);
|
||||
const team = useSelector((state: IRootState) => {
|
||||
return state.entities.teams.data[team_id];
|
||||
return state.entities.teams.data[routeParams.team_id];
|
||||
});
|
||||
const teams = useSelector((state: IRootState) => {
|
||||
return memoizedGetTeams(state.entities.teams.data);
|
||||
});
|
||||
const userTeams = useSelector((state: IRootState) => {
|
||||
return state.auth.user.teams;
|
||||
});
|
||||
const routeTemplate = route && route.path ? route.path : "";
|
||||
|
||||
const [showAddHostsRedirectModal, setShowAddHostsRedirectModal] = useState(
|
||||
false
|
||||
@ -98,7 +128,7 @@ const TeamDetailsWrapper = ({
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const navigateToNav = (i: number): void => {
|
||||
const navPath = teamDetailsSubNav[i].getPathname(team_id);
|
||||
const navPath = teamDetailsSubNav[i].getPathname(routeParams.team_id);
|
||||
dispatch(push(navPath));
|
||||
};
|
||||
|
||||
@ -106,6 +136,8 @@ const TeamDetailsWrapper = ({
|
||||
dispatch(teamActions.loadAll({ perPage: 500 }));
|
||||
}, [dispatch]);
|
||||
|
||||
const [teamMenuIsOpen, setTeamMenuIsOpen] = useState<boolean>(false);
|
||||
|
||||
const toggleAddHostsRedirectModal = useCallback(() => {
|
||||
setShowAddHostsRedirectModal(!showAddHostsRedirectModal);
|
||||
}, [showAddHostsRedirectModal, setShowAddHostsRedirectModal]);
|
||||
@ -153,6 +185,38 @@ const TeamDetailsWrapper = ({
|
||||
[dispatch, toggleEditTeamModal, team]
|
||||
);
|
||||
|
||||
const handleTeamSelect = (teamId: number) => {
|
||||
const selectedTeam = find(teams, ["id", teamId]);
|
||||
const { ADMIN_TEAMS } = PATHS;
|
||||
|
||||
const newRouteParams = {
|
||||
...routeParams,
|
||||
team_id: selectedTeam ? selectedTeam.id : teamId,
|
||||
};
|
||||
|
||||
const nextLocation = getNextLocationPath({
|
||||
pathPrefix: ADMIN_TEAMS,
|
||||
routeTemplate,
|
||||
routeParams: newRouteParams,
|
||||
});
|
||||
|
||||
router.replace(`${nextLocation}/members`);
|
||||
|
||||
setCurrentTeam(selectedTeam);
|
||||
};
|
||||
|
||||
const handleTeamMenuOpen = () => {
|
||||
setTeamMenuIsOpen(true);
|
||||
};
|
||||
|
||||
const handleTeamMenuClose = () => {
|
||||
setTeamMenuIsOpen(false);
|
||||
};
|
||||
|
||||
const teamDetailsClasses = classnames(baseClass, {
|
||||
"team-select-open": teamMenuIsOpen,
|
||||
});
|
||||
|
||||
if (isLoadingTeams || team === undefined) {
|
||||
return (
|
||||
<div className={`${baseClass}__loading-spinner`}>
|
||||
@ -162,17 +226,39 @@ const TeamDetailsWrapper = ({
|
||||
}
|
||||
const hostsCount = team.host_count;
|
||||
const hostsTotalDisplay = hostsCount === 1 ? "1 host" : `${hostsCount} hosts`;
|
||||
const userAdminTeams = userTeams.filter(
|
||||
(thisTeam) => thisTeam.role === "admin"
|
||||
);
|
||||
const adminTeams = isGlobalAdmin ? teams : userAdminTeams;
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={teamDetailsClasses}>
|
||||
<TabsWrapper>
|
||||
<Link to={PATHS.ADMIN_TEAMS} className={`${baseClass}__back-link`}>
|
||||
<img src={BackChevron} alt="back chevron" id="back-chevron" />
|
||||
<span>Back to teams</span>
|
||||
</Link>
|
||||
<>
|
||||
{isGlobalAdmin && (
|
||||
<Link to={PATHS.ADMIN_TEAMS} className={`${baseClass}__back-link`}>
|
||||
<img src={BackChevron} alt="back chevron" id="back-chevron" />
|
||||
<span>Back to teams</span>
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
<div className={`${baseClass}__team-header`}>
|
||||
<div className={`${baseClass}__team-details`}>
|
||||
<h1>{team.name}</h1>
|
||||
{adminTeams.length === 1 ? (
|
||||
<h1>{team.name}</h1>
|
||||
) : (
|
||||
<TeamsDropdown
|
||||
currentTeamId={toNumber(routeParams.team_id)}
|
||||
isLoading={isLoadingTeams}
|
||||
teams={adminTeams || []}
|
||||
hideAllTeamsOption
|
||||
onChange={(newSelectedValue: number) =>
|
||||
handleTeamSelect(newSelectedValue)
|
||||
}
|
||||
onOpen={handleTeamMenuOpen}
|
||||
onClose={handleTeamMenuClose}
|
||||
/>
|
||||
)}
|
||||
<span className={`${baseClass}__host-count`}>
|
||||
{hostsTotalDisplay}
|
||||
</span>
|
||||
@ -185,16 +271,18 @@ const TeamDetailsWrapper = ({
|
||||
Edit team
|
||||
</>
|
||||
</Button>
|
||||
<Button onClick={toggleDeleteTeamModal} variant={"text-icon"}>
|
||||
<>
|
||||
<img src={TrashIcon} alt="Delete team icon" />
|
||||
Delete team
|
||||
</>
|
||||
</Button>
|
||||
{isGlobalAdmin && (
|
||||
<Button onClick={toggleDeleteTeamModal} variant={"text-icon"}>
|
||||
<>
|
||||
<img src={TrashIcon} alt="Delete team icon" />
|
||||
Delete team
|
||||
</>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Tabs
|
||||
selectedIndex={getTabIndex(pathname, team_id)}
|
||||
selectedIndex={getTabIndex(pathname, routeParams.team_id)}
|
||||
onSelect={(i) => navigateToNav(i)}
|
||||
>
|
||||
<TabList>
|
||||
|
@ -1,4 +1,5 @@
|
||||
.team-management {
|
||||
padding: 0;
|
||||
&__page-description {
|
||||
font-size: $x-small;
|
||||
color: $core-fleet-black;
|
||||
@ -7,6 +8,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
.team-details__team-details {
|
||||
.form-field--dropdown {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.team-details.team-select-open {
|
||||
.component__tabs-wrapper {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.members {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.no-teams {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -21,24 +21,18 @@ import teamActions from "redux/nodes/entities/teams/actions";
|
||||
import TableContainer from "components/TableContainer";
|
||||
import TableDataError from "components/TableDataError";
|
||||
import Modal from "components/modals/Modal";
|
||||
import Spinner from "components/loaders/Spinner";
|
||||
import UserForm from "./components/UserForm";
|
||||
import { DEFAULT_CREATE_USER_ERRORS } from "utilities/constants";
|
||||
import EmptyUsers from "./components/EmptyUsers";
|
||||
import { generateTableHeaders, combineDataSets } from "./UsersTableConfig";
|
||||
import DeleteUserForm from "./components/DeleteUserForm";
|
||||
import ResetPasswordModal from "./components/ResetPasswordModal";
|
||||
import ResetSessionsModal from "./components/ResetSessionsModal";
|
||||
import { NewUserType } from "./components/UserForm/UserForm";
|
||||
import CreateUserModal from "../UserManagementPage/components/CreateUserModal";
|
||||
import EditUserModal from "../UserManagementPage/components/EditUserModal";
|
||||
|
||||
const baseClass = "user-management";
|
||||
|
||||
const DEFAULT_CREATE_USER_ERRORS = {
|
||||
email: null,
|
||||
name: null,
|
||||
password: null,
|
||||
sso_enabled: null,
|
||||
};
|
||||
|
||||
const generateUpdateData = (currentUserData, formData) => {
|
||||
const updatableFields = [
|
||||
"global_role",
|
||||
@ -92,7 +86,7 @@ export class UserManagementPage extends Component {
|
||||
isFormSubmitting: false,
|
||||
userEditing: null,
|
||||
usersEditing: [],
|
||||
createUserErrors: DEFAULT_CREATE_USER_ERRORS,
|
||||
createUserErrors: { DEFAULT_CREATE_USER_ERRORS },
|
||||
};
|
||||
}
|
||||
|
||||
@ -154,7 +148,6 @@ export class UserManagementPage extends Component {
|
||||
|
||||
onCreateUserSubmit = (formData) => {
|
||||
const { dispatch, config } = this.props;
|
||||
const { createUserErrors } = this.state;
|
||||
|
||||
this.setState({ isFormSubmitting: true });
|
||||
|
||||
@ -179,12 +172,12 @@ export class UserManagementPage extends Component {
|
||||
})
|
||||
.catch((userErrors) => {
|
||||
if (userErrors.base.includes("Duplicate")) {
|
||||
this.setState({
|
||||
createUserErrors: {
|
||||
...createUserErrors,
|
||||
email: "A user with this email address already exists",
|
||||
},
|
||||
});
|
||||
dispatch(
|
||||
renderFlash(
|
||||
"error",
|
||||
"A user with this email address already exists."
|
||||
)
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
renderFlash("error", "Could not create user. Please try again.")
|
||||
@ -210,12 +203,12 @@ export class UserManagementPage extends Component {
|
||||
})
|
||||
.catch((userErrors) => {
|
||||
if (userErrors.base.includes("Duplicate")) {
|
||||
this.setState({
|
||||
createUserErrors: {
|
||||
...createUserErrors,
|
||||
email: "A user with this email address already exists",
|
||||
},
|
||||
});
|
||||
dispatch(
|
||||
renderFlash(
|
||||
"error",
|
||||
"A user with this email address already exists."
|
||||
)
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
renderFlash("error", "Could not create user. Please try again.")
|
||||
@ -459,7 +452,7 @@ export class UserManagementPage extends Component {
|
||||
teams,
|
||||
isPremiumTier,
|
||||
} = this.props;
|
||||
const { showEditUserModal, userEditing, isFormSubmitting } = this.state;
|
||||
const { showEditUserModal, userEditing } = this.state;
|
||||
const { onEditUser, toggleEditUserModal, getUser } = this;
|
||||
|
||||
if (!showEditUserModal) return null;
|
||||
@ -473,13 +466,8 @@ export class UserManagementPage extends Component {
|
||||
className={`${baseClass}__edit-user-modal`}
|
||||
>
|
||||
<>
|
||||
{isFormSubmitting && (
|
||||
<div className="loading-spinner">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
<UserForm
|
||||
serverErrors={inviteErrors}
|
||||
<EditUserModal
|
||||
serverError={inviteErrors}
|
||||
defaultEmail={userData.email}
|
||||
defaultName={userData.name}
|
||||
defaultGlobalRole={userData.global_role}
|
||||
@ -493,6 +481,7 @@ export class UserManagementPage extends Component {
|
||||
smtpConfigured={config.configured}
|
||||
canUseSso={config.enable_sso}
|
||||
isSsoEnabled={userData.sso_enabled}
|
||||
isModifiedByGlobalAdmin
|
||||
/>
|
||||
</>
|
||||
</Modal>
|
||||
@ -500,45 +489,36 @@ export class UserManagementPage extends Component {
|
||||
};
|
||||
|
||||
renderCreateUserModal = () => {
|
||||
const { currentUser, config, teams, isPremiumTier } = this.props;
|
||||
const {
|
||||
showCreateUserModal,
|
||||
createUserErrors,
|
||||
isFormSubmitting,
|
||||
} = this.state;
|
||||
currentUser,
|
||||
config,
|
||||
teams,
|
||||
userErrors,
|
||||
isPremiumTier,
|
||||
} = this.props;
|
||||
const { showCreateUserModal, isFormSubmitting } = this.state;
|
||||
const { onCreateUserSubmit, toggleCreateUserModal } = this;
|
||||
|
||||
if (!showCreateUserModal) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Create user"
|
||||
onExit={toggleCreateUserModal}
|
||||
className={`${baseClass}__create-user-modal`}
|
||||
>
|
||||
<>
|
||||
{isFormSubmitting && (
|
||||
<div className="loading-spinner">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
<UserForm
|
||||
serverErrors={createUserErrors}
|
||||
currentUserId={currentUser.id}
|
||||
onCancel={toggleCreateUserModal}
|
||||
onSubmit={onCreateUserSubmit}
|
||||
availableTeams={teams}
|
||||
defaultGlobalRole={"observer"}
|
||||
defaultTeams={[]}
|
||||
defaultNewUserType={false}
|
||||
submitText={"Create"}
|
||||
isPremiumTier={isPremiumTier}
|
||||
smtpConfigured={config.configured}
|
||||
canUseSso={config.enable_sso}
|
||||
isNewUser
|
||||
/>
|
||||
</>
|
||||
</Modal>
|
||||
<CreateUserModal
|
||||
serverError={userErrors}
|
||||
currentUserId={currentUser.id}
|
||||
onCancel={toggleCreateUserModal}
|
||||
onSubmit={onCreateUserSubmit}
|
||||
availableTeams={teams}
|
||||
defaultGlobalRole={"observer"}
|
||||
defaultTeams={[]}
|
||||
defaultNewUserType={false}
|
||||
submitText={"Create"}
|
||||
isPremiumTier={isPremiumTier}
|
||||
smtpConfigured={config.configured}
|
||||
canUseSso={config.enable_sso}
|
||||
isFormSubmitting={isFormSubmitting}
|
||||
isModifiedByGlobalAdmin
|
||||
isNewUser
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
.user-management {
|
||||
padding: $pad-xlarge;
|
||||
|
||||
padding: 0;
|
||||
&__page-description {
|
||||
font-size: $x-small;
|
||||
color: $core-fleet-black;
|
||||
@ -129,20 +128,4 @@
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
&__create-user-modal,
|
||||
&__edit-user-modal {
|
||||
.loading-spinner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(255,255,255,0.75);
|
||||
z-index: 1;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,72 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { IUserFormErrors } from "interfaces/user";
|
||||
import Modal from "components/modals/Modal";
|
||||
import Spinner from "components/loaders/Spinner";
|
||||
import UserForm from "../UserForm";
|
||||
import { IFormData } from "../UserForm/UserForm";
|
||||
|
||||
interface ICreateUserModalProps {
|
||||
onCancel: () => void;
|
||||
onSubmit: (formData: IFormData) => void;
|
||||
defaultGlobalRole?: string | null;
|
||||
defaultTeamRole?: string;
|
||||
defaultTeams?: ITeam[];
|
||||
availableTeams: ITeam[];
|
||||
isPremiumTier: boolean;
|
||||
smtpConfigured: boolean;
|
||||
currentTeam?: ITeam;
|
||||
canUseSso: boolean; // corresponds to whether SSO is enabled for the organization
|
||||
isModifiedByGlobalAdmin?: boolean | false;
|
||||
isFormSubmitting?: boolean | false;
|
||||
serverErrors?: IUserFormErrors;
|
||||
}
|
||||
|
||||
const baseClass = "create-user-modal";
|
||||
|
||||
const CreateUserModal = ({
|
||||
onCancel,
|
||||
onSubmit,
|
||||
currentTeam,
|
||||
defaultGlobalRole,
|
||||
defaultTeamRole,
|
||||
defaultTeams,
|
||||
availableTeams,
|
||||
isPremiumTier,
|
||||
smtpConfigured,
|
||||
canUseSso,
|
||||
isModifiedByGlobalAdmin,
|
||||
isFormSubmitting,
|
||||
serverErrors,
|
||||
}: ICreateUserModalProps): JSX.Element => {
|
||||
return (
|
||||
<Modal title="Create user" onExit={onCancel} className={baseClass}>
|
||||
<>
|
||||
{isFormSubmitting && (
|
||||
<div className="loading-spinner">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
<UserForm
|
||||
serverErrors={serverErrors}
|
||||
defaultGlobalRole={defaultGlobalRole}
|
||||
defaultTeamRole={defaultTeamRole}
|
||||
defaultTeams={defaultTeams}
|
||||
onCancel={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
availableTeams={availableTeams}
|
||||
submitText={"Create"}
|
||||
isPremiumTier={isPremiumTier}
|
||||
smtpConfigured={smtpConfigured}
|
||||
canUseSso={canUseSso}
|
||||
isModifiedByGlobalAdmin={isModifiedByGlobalAdmin}
|
||||
currentTeam={currentTeam}
|
||||
isNewUser
|
||||
/>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateUserModal;
|
@ -0,0 +1,15 @@
|
||||
.create-user-modal {
|
||||
.loading-spinner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
z-index: 1;
|
||||
border-radius: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from "./CreateUserModal";
|
@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { IUserFormErrors } from "interfaces/user";
|
||||
import Modal from "components/modals/Modal";
|
||||
import UserForm from "../UserForm";
|
||||
import { IFormData } from "../UserForm/UserForm";
|
||||
@ -11,12 +12,16 @@ interface IEditUserModalProps {
|
||||
defaultName?: string;
|
||||
defaultEmail?: string;
|
||||
defaultGlobalRole?: string | null;
|
||||
defaultTeamRole?: string;
|
||||
defaultTeams?: ITeam[];
|
||||
availableTeams: ITeam[];
|
||||
currentTeam: ITeam;
|
||||
isPremiumTier: boolean;
|
||||
smtpConfigured: boolean;
|
||||
canUseSso: boolean; // corresponds to whether SSO is enabled for the organization
|
||||
isSsoEnabled?: boolean; // corresponds to whether SSO is enabled for the individual user
|
||||
serverErrors?: IUserFormErrors;
|
||||
isModifiedByGlobalAdmin?: boolean | false;
|
||||
}
|
||||
|
||||
const baseClass = "edit-user-modal";
|
||||
@ -27,12 +32,16 @@ const EditUserModal = ({
|
||||
defaultName,
|
||||
defaultEmail,
|
||||
defaultGlobalRole,
|
||||
defaultTeamRole,
|
||||
defaultTeams,
|
||||
availableTeams,
|
||||
isPremiumTier,
|
||||
smtpConfigured,
|
||||
canUseSso,
|
||||
isSsoEnabled,
|
||||
isModifiedByGlobalAdmin,
|
||||
currentTeam,
|
||||
serverErrors,
|
||||
}: IEditUserModalProps): JSX.Element => {
|
||||
return (
|
||||
<Modal
|
||||
@ -41,9 +50,11 @@ const EditUserModal = ({
|
||||
className={`${baseClass}__edit-user-modal`}
|
||||
>
|
||||
<UserForm
|
||||
serverErrors={serverErrors}
|
||||
defaultName={defaultName}
|
||||
defaultEmail={defaultEmail}
|
||||
defaultGlobalRole={defaultGlobalRole}
|
||||
defaultTeamRole={defaultTeamRole}
|
||||
defaultTeams={defaultTeams}
|
||||
onCancel={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
@ -53,6 +64,8 @@ const EditUserModal = ({
|
||||
smtpConfigured={smtpConfigured}
|
||||
canUseSso={canUseSso}
|
||||
isSsoEnabled={isSsoEnabled}
|
||||
isModifiedByGlobalAdmin={isModifiedByGlobalAdmin}
|
||||
currentTeam={currentTeam}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
|
@ -0,0 +1,16 @@
|
||||
.edit-user-modal {
|
||||
position: relative;
|
||||
.loading-spinner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
z-index: 1;
|
||||
border-radius: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { IRole } from "interfaces/role";
|
||||
// ignore TS error for now until these are rewritten in ts.
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
|
||||
interface ISelectRoleFormProps {
|
||||
defaultTeamRole: string;
|
||||
currentTeam: ITeam;
|
||||
teams: ITeam[];
|
||||
onFormChange: (teams: ITeam[]) => void;
|
||||
}
|
||||
|
||||
const baseClass = "select-role-form";
|
||||
|
||||
const roles: IRole[] = [
|
||||
{
|
||||
disabled: false,
|
||||
label: "Observer",
|
||||
value: "observer",
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
label: "Maintainer",
|
||||
value: "maintainer",
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
label: "Admin",
|
||||
value: "admin",
|
||||
},
|
||||
];
|
||||
|
||||
const generateSelectedTeamData = (
|
||||
allTeams: ITeam[],
|
||||
updatedTeam: ITeam
|
||||
): ITeam[] => {
|
||||
const filtered = allTeams.map(
|
||||
(teamItem): ITeam => {
|
||||
const teamRole =
|
||||
teamItem.id === updatedTeam.id ? updatedTeam.role : teamItem.role;
|
||||
return {
|
||||
description: teamItem.description,
|
||||
id: teamItem.id,
|
||||
host_count: teamItem.host_count,
|
||||
user_count: teamItem.user_count,
|
||||
name: teamItem.name,
|
||||
role: teamRole,
|
||||
};
|
||||
}
|
||||
);
|
||||
return filtered;
|
||||
};
|
||||
|
||||
const SelectRoleForm = ({
|
||||
defaultTeamRole,
|
||||
currentTeam,
|
||||
teams,
|
||||
onFormChange,
|
||||
}: ISelectRoleFormProps): JSX.Element => {
|
||||
const [selectedRole, setSelectedRole] = useState<string>(
|
||||
defaultTeamRole.toLowerCase()
|
||||
);
|
||||
|
||||
const updateSelectedRole = (newRoleValue: string) => {
|
||||
const updatedTeam = { ...currentTeam };
|
||||
|
||||
updatedTeam.role = newRoleValue;
|
||||
|
||||
onFormChange(generateSelectedTeamData(teams, updatedTeam));
|
||||
|
||||
setSelectedRole(newRoleValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__select-role`}>
|
||||
<Dropdown
|
||||
value={selectedRole}
|
||||
className={`${baseClass}__role-dropdown`}
|
||||
options={roles}
|
||||
searchable={false}
|
||||
onChange={(newRoleValue: string) => updateSelectedRole(newRoleValue)}
|
||||
testId={`${name}-checkbox`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectRoleForm;
|
@ -30,6 +30,11 @@ const roles = [
|
||||
label: "Maintainer",
|
||||
value: "maintainer",
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
label: "Admin",
|
||||
value: "admin",
|
||||
},
|
||||
];
|
||||
|
||||
const generateFormListItems = (
|
||||
|
@ -4,6 +4,7 @@ import { Link } from "react-router";
|
||||
import PATHS from "router/paths";
|
||||
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { IUserFormErrors } from "interfaces/user";
|
||||
import Button from "components/buttons/Button";
|
||||
import validatePresence from "components/forms/validators/validate_presence";
|
||||
import validEmail from "components/forms/validators/valid_email";
|
||||
@ -25,6 +26,7 @@ import Radio from "components/forms/fields/Radio";
|
||||
import InfoBanner from "components/InfoBanner/InfoBanner";
|
||||
import SelectedTeamsForm from "../SelectedTeamsForm/SelectedTeamsForm";
|
||||
import OpenNewTabIcon from "../../../../../../assets/images/open-new-tab-12x12@2x.png";
|
||||
import SelectRoleForm from "../SelectRoleForm/SelectRoleForm";
|
||||
|
||||
const baseClass = "create-user-form";
|
||||
|
||||
@ -68,13 +70,6 @@ export interface IFormData {
|
||||
invited_by?: number;
|
||||
}
|
||||
|
||||
interface IUserFormErrors {
|
||||
email: string | null;
|
||||
name: string | null;
|
||||
password: string | null;
|
||||
sso_enabled: boolean | null;
|
||||
}
|
||||
|
||||
interface ICreateUserFormProps {
|
||||
availableTeams: ITeam[];
|
||||
onCancel: () => void;
|
||||
@ -83,7 +78,10 @@ interface ICreateUserFormProps {
|
||||
defaultName?: string;
|
||||
defaultEmail?: string;
|
||||
currentUserId?: number;
|
||||
currentTeam?: ITeam;
|
||||
isModifiedByGlobalAdmin?: boolean | false;
|
||||
defaultGlobalRole?: string | null;
|
||||
defaultTeamRole?: string;
|
||||
defaultTeams?: ITeam[];
|
||||
isPremiumTier: boolean;
|
||||
smtpConfigured?: boolean;
|
||||
@ -185,6 +183,16 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
|
||||
});
|
||||
};
|
||||
|
||||
onTeamRoleChange = (teams: ITeam[]): void => {
|
||||
const { formData } = this.state;
|
||||
this.setState({
|
||||
formData: {
|
||||
...formData,
|
||||
teams,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onFormSubmit = (evt: FormEvent): void => {
|
||||
const { createSubmitData, validate } = this;
|
||||
evt.preventDefault();
|
||||
@ -291,9 +299,13 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
|
||||
renderGlobalRoleForm = (): JSX.Element => {
|
||||
const { onGlobalUserRoleChange } = this;
|
||||
const {
|
||||
formData: { global_role },
|
||||
formData: { global_role, teams },
|
||||
} = this.state;
|
||||
const { isPremiumTier } = this.props;
|
||||
const {
|
||||
availableTeams,
|
||||
isModifiedByGlobalAdmin,
|
||||
isPremiumTier,
|
||||
} = this.props;
|
||||
return (
|
||||
<>
|
||||
{isPremiumTier && (
|
||||
@ -346,37 +358,58 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
|
||||
};
|
||||
|
||||
renderTeamsForm = (): JSX.Element => {
|
||||
const { onSelectedTeamChange, renderNoTeamsMessage } = this;
|
||||
const { availableTeams, isPremiumTier } = this.props;
|
||||
const {
|
||||
onSelectedTeamChange,
|
||||
renderNoTeamsMessage,
|
||||
onTeamRoleChange,
|
||||
} = this;
|
||||
const {
|
||||
availableTeams,
|
||||
isModifiedByGlobalAdmin,
|
||||
defaultTeamRole,
|
||||
currentTeam,
|
||||
} = this.props;
|
||||
const {
|
||||
formData: { teams },
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
<InfoBanner className={`${baseClass}__user-permissions-info`}>
|
||||
<p>
|
||||
Users can be members of multiple teams and can only manage or
|
||||
observe team-specific users, entities, and settings in Fleet.
|
||||
</p>
|
||||
<a
|
||||
href="https://fleetdm.com/docs/using-fleet/permissions#team-member-permissions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn more about user permissions
|
||||
<img src={OpenNewTabIcon} alt="open new tab" />
|
||||
</a>
|
||||
</InfoBanner>
|
||||
{availableTeams.length > 0 ? (
|
||||
<SelectedTeamsForm
|
||||
availableTeams={availableTeams}
|
||||
usersCurrentTeams={teams}
|
||||
onFormChange={onSelectedTeamChange}
|
||||
/>
|
||||
) : (
|
||||
renderNoTeamsMessage()
|
||||
)}
|
||||
{availableTeams.length &&
|
||||
(isModifiedByGlobalAdmin ? (
|
||||
<>
|
||||
<InfoBanner className={`${baseClass}__user-permissions-info`}>
|
||||
<p>
|
||||
Users can be members of multiple teams and can only manage or
|
||||
observe team-specific users, entities, and settings in Fleet.
|
||||
</p>
|
||||
<a
|
||||
href="https://fleetdm.com/docs/using-fleet/permissions#team-member-permissions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn more about user permissions
|
||||
<img src={OpenNewTabIcon} alt="open new tab" />
|
||||
</a>
|
||||
</InfoBanner>
|
||||
<SelectedTeamsForm
|
||||
availableTeams={availableTeams}
|
||||
usersCurrentTeams={teams}
|
||||
onFormChange={onSelectedTeamChange}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className={`${baseClass}__label`}>Role</p>
|
||||
<SelectRoleForm
|
||||
currentTeam={currentTeam || teams[0]}
|
||||
teams={teams}
|
||||
defaultTeamRole={defaultTeamRole || "observer"}
|
||||
onFormChange={onTeamRoleChange}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
{!availableTeams.length && renderNoTeamsMessage()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -394,6 +427,8 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
|
||||
smtpConfigured,
|
||||
canUseSso,
|
||||
isNewUser,
|
||||
currentTeam,
|
||||
isModifiedByGlobalAdmin,
|
||||
serverErrors,
|
||||
} = this.props;
|
||||
const {
|
||||
@ -422,7 +457,7 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
|
||||
name="name"
|
||||
onChange={onInputChange("name")}
|
||||
placeholder="Full name"
|
||||
value={name}
|
||||
value={name || ""}
|
||||
/>
|
||||
<div
|
||||
className="email-disabled"
|
||||
@ -435,7 +470,7 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
|
||||
name="email"
|
||||
onChange={onInputChange("email")}
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
value={email || ""}
|
||||
disabled={!isNewUser && !smtpConfigured}
|
||||
/>
|
||||
</div>
|
||||
@ -498,53 +533,65 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
|
||||
{isNewUser && (
|
||||
<div className={`${baseClass}__new-user-container`}>
|
||||
<div className={`${baseClass}__new-user-radios`}>
|
||||
<Radio
|
||||
className={`${baseClass}__radio-input`}
|
||||
label={"Create user"}
|
||||
id={"create-user"}
|
||||
checked={newUserType !== NewUserType.AdminInvited}
|
||||
value={NewUserType.AdminCreated}
|
||||
name={"newUserType"}
|
||||
onChange={onRadioChange("newUserType")}
|
||||
/>
|
||||
<div
|
||||
className="invite-disabled"
|
||||
data-tip
|
||||
data-for="invite-disabled-tooltip"
|
||||
data-tip-disable={smtpConfigured}
|
||||
>
|
||||
<Radio
|
||||
className={`${baseClass}__radio-input`}
|
||||
label={"Invite user"}
|
||||
id={"invite-user"}
|
||||
disabled={!smtpConfigured}
|
||||
checked={newUserType === NewUserType.AdminInvited}
|
||||
value={NewUserType.AdminInvited}
|
||||
{isModifiedByGlobalAdmin ? (
|
||||
<>
|
||||
<Radio
|
||||
className={`${baseClass}__radio-input`}
|
||||
label={"Create user"}
|
||||
id={"create-user"}
|
||||
checked={newUserType !== NewUserType.AdminInvited}
|
||||
value={NewUserType.AdminCreated}
|
||||
name={"newUserType"}
|
||||
onChange={onRadioChange("newUserType")}
|
||||
/>
|
||||
<div
|
||||
className="invite-disabled"
|
||||
data-tip
|
||||
data-for="invite-disabled-tooltip"
|
||||
data-tip-disable={smtpConfigured}
|
||||
>
|
||||
<Radio
|
||||
className={`${baseClass}__radio-input`}
|
||||
label={"Invite user"}
|
||||
id={"invite-user"}
|
||||
disabled={!smtpConfigured}
|
||||
checked={newUserType === NewUserType.AdminInvited}
|
||||
value={NewUserType.AdminInvited}
|
||||
name={"newUserType"}
|
||||
onChange={onRadioChange("newUserType")}
|
||||
/>
|
||||
<ReactTooltip
|
||||
place="bottom"
|
||||
type="dark"
|
||||
effect="solid"
|
||||
id="invite-disabled-tooltip"
|
||||
backgroundColor="#3e4771"
|
||||
data-html
|
||||
>
|
||||
<span className={`${baseClass}__tooltip-text`}>
|
||||
The "Invite user" feature requires that SMTP
|
||||
is
|
||||
<br />
|
||||
configured in order to send invitation emails. <br />
|
||||
<br />
|
||||
SMTP can be configured in{" "}
|
||||
<strong>
|
||||
Settings > <br />
|
||||
Organization settings
|
||||
</strong>
|
||||
.
|
||||
</span>
|
||||
</ReactTooltip>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<input
|
||||
type="hidden"
|
||||
id={"create-user"}
|
||||
value={NewUserType.AdminCreated}
|
||||
name={"newUserType"}
|
||||
onChange={onRadioChange("newUserType")}
|
||||
/>
|
||||
<ReactTooltip
|
||||
place="bottom"
|
||||
type="dark"
|
||||
effect="solid"
|
||||
id="invite-disabled-tooltip"
|
||||
backgroundColor="#3e4771"
|
||||
data-html
|
||||
>
|
||||
<span className={`${baseClass}__tooltip-text`}>
|
||||
The "Invite user" feature requires that SMTP is
|
||||
<br />
|
||||
configured in order to send invitation emails. <br />
|
||||
<br />
|
||||
SMTP can be configured in{" "}
|
||||
<strong>
|
||||
Settings > <br />
|
||||
Organization settings
|
||||
</strong>
|
||||
.
|
||||
</span>
|
||||
</ReactTooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{newUserType !== NewUserType.AdminInvited && !sso_enabled && (
|
||||
<>
|
||||
@ -554,7 +601,7 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
|
||||
name="password"
|
||||
onChange={onInputChange("password")}
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
value={password || ""}
|
||||
type="password"
|
||||
hint={[
|
||||
"Must include 7 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
|
||||
@ -580,24 +627,32 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
|
||||
<div className={`${baseClass}__selected-teams-container`}>
|
||||
<div className={`${baseClass}__team-radios`}>
|
||||
<p className={`${baseClass}__label`}>Team</p>
|
||||
<Radio
|
||||
className={`${baseClass}__radio-input`}
|
||||
label={"Global user"}
|
||||
id={"global-user"}
|
||||
checked={isGlobalUser}
|
||||
value={UserTeamType.GlobalUser}
|
||||
name={"userTeamType"}
|
||||
onChange={onIsGlobalUserChange}
|
||||
/>
|
||||
<Radio
|
||||
className={`${baseClass}__radio-input`}
|
||||
label={"Assign teams"}
|
||||
id={"assign-teams"}
|
||||
checked={!isGlobalUser}
|
||||
value={UserTeamType.AssignTeams}
|
||||
name={"userTeamType"}
|
||||
onChange={onIsGlobalUserChange}
|
||||
/>
|
||||
{isModifiedByGlobalAdmin ? (
|
||||
<>
|
||||
<Radio
|
||||
className={`${baseClass}__radio-input`}
|
||||
label={"Global user"}
|
||||
id={"global-user"}
|
||||
checked={isGlobalUser}
|
||||
value={UserTeamType.GlobalUser}
|
||||
name={"userTeamType"}
|
||||
onChange={onIsGlobalUserChange}
|
||||
/>
|
||||
<Radio
|
||||
className={`${baseClass}__radio-input`}
|
||||
label={"Assign teams"}
|
||||
id={"assign-teams"}
|
||||
checked={!isGlobalUser}
|
||||
value={UserTeamType.AssignTeams}
|
||||
name={"userTeamType"}
|
||||
onChange={onIsGlobalUserChange}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<p className="current-team">
|
||||
{currentTeam ? currentTeam.name : ""}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${baseClass}__teams-form-container`}>
|
||||
{isGlobalUser ? renderGlobalRoleForm() : renderTeamsForm()}
|
||||
|
@ -45,6 +45,11 @@
|
||||
width: 310px;
|
||||
}
|
||||
|
||||
.current-team {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
color: $core-fleet-black;
|
||||
font-size: $x-small;
|
||||
|
@ -1,4 +1,12 @@
|
||||
import { isEqual } from "lodash";
|
||||
import {
|
||||
isEmpty,
|
||||
isEqual,
|
||||
isPlainObject,
|
||||
isString,
|
||||
reduce,
|
||||
trim,
|
||||
union,
|
||||
} from "lodash";
|
||||
|
||||
import { IInvite } from "interfaces/invite";
|
||||
import { IUser, IUserUpdateBody } from "interfaces/user";
|
||||
@ -9,6 +17,12 @@ type ICurrentUserData = Pick<
|
||||
"global_role" | "teams" | "name" | "email" | "sso_enabled"
|
||||
>;
|
||||
|
||||
interface ILocationParams {
|
||||
pathPrefix?: string;
|
||||
routeTemplate?: string;
|
||||
routeParams?: { [key: string]: any };
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that will compare the current user with data from the editing
|
||||
* form and return an object with the difference between the two. This can be
|
||||
@ -49,6 +63,36 @@ const generateUpdateData = (
|
||||
);
|
||||
};
|
||||
|
||||
export const getNextLocationPath = ({
|
||||
pathPrefix = "",
|
||||
routeTemplate = "",
|
||||
routeParams = {},
|
||||
}: ILocationParams): string => {
|
||||
const pathPrefixFinal = isString(pathPrefix) ? pathPrefix : "";
|
||||
const routeTemplateFinal = (isString(routeTemplate) && routeTemplate) || "";
|
||||
const routeParamsFinal = isPlainObject(routeParams) ? routeParams : {};
|
||||
|
||||
let routeString = "";
|
||||
|
||||
if (!isEmpty(routeParamsFinal)) {
|
||||
routeString = reduce(
|
||||
routeParamsFinal,
|
||||
(string, value, key) => {
|
||||
return string.replace(`:${key}`, encodeURIComponent(value));
|
||||
},
|
||||
routeTemplateFinal
|
||||
);
|
||||
}
|
||||
|
||||
const nextLocation = union(
|
||||
trim(pathPrefixFinal, "/").split("/"),
|
||||
routeString.split("/")
|
||||
).join("/");
|
||||
|
||||
return `/${nextLocation}`;
|
||||
};
|
||||
|
||||
export default {
|
||||
generateUpdateData,
|
||||
getNextLocationPath,
|
||||
};
|
||||
|
@ -123,6 +123,8 @@ const ManageHostsPage = ({
|
||||
isGlobalMaintainer,
|
||||
isAnyTeamMaintainer,
|
||||
isTeamMaintainer,
|
||||
isAnyTeamAdmin,
|
||||
isTeamAdmin,
|
||||
isOnGlobalTeam,
|
||||
isOnlyObserver,
|
||||
isPremiumTier,
|
||||
@ -213,9 +215,12 @@ const ManageHostsPage = ({
|
||||
// ===== end filter matching
|
||||
|
||||
const canAddNewHosts =
|
||||
isGlobalAdmin || isGlobalMaintainer || isAnyTeamMaintainer;
|
||||
isGlobalAdmin ||
|
||||
isGlobalMaintainer ||
|
||||
isAnyTeamAdmin ||
|
||||
isAnyTeamMaintainer;
|
||||
const canEnrollHosts =
|
||||
isGlobalAdmin || isGlobalMaintainer || isTeamMaintainer;
|
||||
isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer;
|
||||
const canAddNewLabels = isGlobalAdmin || isGlobalMaintainer;
|
||||
|
||||
const generateInstallerTeam = currentTeam || {
|
||||
|
@ -17,9 +17,10 @@ import AdminTeamManagementPage from "pages/admin/TeamManagementPage";
|
||||
import TeamDetailsWrapper from "pages/admin/TeamManagementPage/TeamDetailsWrapper";
|
||||
import App from "components/App";
|
||||
import AuthenticatedAdminRoutes from "components/AuthenticatedAdminRoutes";
|
||||
import AuthAnyAdminRoutes from "components/AuthAnyAdminRoutes";
|
||||
import AuthenticatedRoutes from "components/AuthenticatedRoutes";
|
||||
import AuthGlobalAdminMaintainerRoutes from "components/AuthGlobalAdminMaintainerRoutes";
|
||||
import AuthAnyMaintainerGlobalAdminRoutes from "components/AuthAnyMaintainerGlobalAdminRoutes";
|
||||
import AuthAnyMaintainerAnyAdminRoutes from "components/AuthAnyMaintainerAnyAdminRoutes";
|
||||
import PremiumTierRoutes from "components/PremiumTierRoutes";
|
||||
import ConfirmInvitePage from "pages/ConfirmInvitePage";
|
||||
import ConfirmSSOInvitePage from "pages/ConfirmSSOInvitePage";
|
||||
@ -88,12 +89,14 @@ const routes = (
|
||||
<Route component={CoreLayout}>
|
||||
<IndexRedirect to={"dashboard"} />
|
||||
<Route path="dashboard" component={Homepage} />
|
||||
<Route path="settings" component={AuthenticatedAdminRoutes}>
|
||||
<Route path="settings" component={AuthAnyAdminRoutes}>
|
||||
<Route component={SettingsWrapper}>
|
||||
<Route path="organization" component={AdminAppSettingsPage} />
|
||||
<Route path="users" component={AdminUserManagementPage} />
|
||||
<Route component={PremiumTierRoutes}>
|
||||
<Route path="teams" component={AdminTeamManagementPage} />
|
||||
<Route component={AuthenticatedAdminRoutes}>
|
||||
<Route path="organization" component={AdminAppSettingsPage} />
|
||||
<Route path="users" component={AdminUserManagementPage} />
|
||||
<Route component={PremiumTierRoutes}>
|
||||
<Route path="teams" component={AdminTeamManagementPage} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="teams/:team_id" component={TeamDetailsWrapper}>
|
||||
@ -128,7 +131,7 @@ const routes = (
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route component={AuthAnyMaintainerGlobalAdminRoutes}>
|
||||
<Route component={AuthAnyMaintainerAnyAdminRoutes}>
|
||||
<Route path="schedule" component={SchedulePageWrapper}>
|
||||
<Route path="manage" component={ManageSchedulePage} />
|
||||
<Route
|
||||
@ -139,7 +142,7 @@ const routes = (
|
||||
</Route>
|
||||
<Route path="queries" component={QueryPageWrapper}>
|
||||
<Route path="manage" component={ManageQueriesPage} />
|
||||
<Route component={AuthAnyMaintainerGlobalAdminRoutes}>
|
||||
<Route component={AuthAnyMaintainerAnyAdminRoutes}>
|
||||
<Route path="new" component={QueryPage} />
|
||||
</Route>
|
||||
<Route path=":id" component={QueryPage} />
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { IHost } from "../interfaces/host";
|
||||
import { IPack } from "../interfaces/pack";
|
||||
import { IQuery } from "../interfaces/query";
|
||||
import URL_PREFIX from "./url_prefix";
|
||||
|
||||
|
@ -214,6 +214,7 @@ export const userStub: IUser = {
|
||||
id: 1,
|
||||
name: "Gnar Mike",
|
||||
email: "hi@gnar.dog",
|
||||
role: "Observer",
|
||||
global_role: null,
|
||||
api_only: false,
|
||||
force_password_reset: false,
|
||||
|
@ -150,3 +150,10 @@ export const PLATFORM_DROPDOWN_OPTIONS = [
|
||||
{ label: "Linux", value: "linux" },
|
||||
{ label: "macOS", value: "darwin" },
|
||||
];
|
||||
|
||||
export const DEFAULT_CREATE_USER_ERRORS = {
|
||||
email: "",
|
||||
name: "",
|
||||
password: "",
|
||||
sso_enabled: null,
|
||||
};
|
||||
|
@ -39,6 +39,11 @@ const isTeamMaintainer = (
|
||||
return userTeamRole === "maintainer";
|
||||
};
|
||||
|
||||
const isTeamAdmin = (user: IUser | null, teamId: number | null): boolean => {
|
||||
const userTeamRole = user?.teams.find((team) => team.id === teamId)?.role;
|
||||
return userTeamRole === "admin";
|
||||
};
|
||||
|
||||
// This checks against all teams
|
||||
const isAnyTeamMaintainer = (user: IUser): boolean => {
|
||||
if (!isOnGlobalTeam(user)) {
|
||||
@ -48,6 +53,14 @@ const isAnyTeamMaintainer = (user: IUser): boolean => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const isAnyTeamAdmin = (user: IUser): boolean => {
|
||||
if (!isOnGlobalTeam(user)) {
|
||||
return user.teams.some((team) => team?.role === "admin");
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const isOnlyObserver = (user: IUser): boolean => {
|
||||
if (isGlobalObserver(user)) {
|
||||
return true;
|
||||
@ -71,5 +84,7 @@ export default {
|
||||
isTeamObserver,
|
||||
isTeamMaintainer,
|
||||
isAnyTeamMaintainer,
|
||||
isTeamAdmin,
|
||||
isAnyTeamAdmin,
|
||||
isOnlyObserver,
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user