mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
Fix unreleased UI bugs in login page, top nav, and default team (#10928)
This commit is contained in:
parent
23fe4bc718
commit
d3bbed34ec
@ -33,7 +33,7 @@ interface ISiteTopNavProps {
|
||||
|
||||
const REGEX_DETAIL_PAGES = {
|
||||
HOST_DETAILS: /\/hosts\/\d+/i,
|
||||
LABEL_EDIT: /(?<!manage)\/labels\/\d+/i, // Note: we want this to match "/labels/10" but not "/hosts/manage/labels/10"
|
||||
LABEL_EDIT: /\/labels\/\d+/i,
|
||||
LABEL_NEW: /\/labels\/new/i,
|
||||
PACK_EDIT: /\/packs\/\d+/i,
|
||||
PACK_NEW: /\/packs\/new/i,
|
||||
@ -54,8 +54,18 @@ const REGEX_GLOBAL_PAGES = {
|
||||
PROFILE: /\/profile/i,
|
||||
};
|
||||
|
||||
const testDetailPage = (path: string, re: RegExp) => {
|
||||
if (re === REGEX_DETAIL_PAGES.LABEL_EDIT) {
|
||||
// we want to match "/labels/10" but not "/hosts/manage/labels/10"
|
||||
return path.match(re) && !path.match(/\/hosts\/manage\/labels\/\d+/); // we're using this approach because some browsers don't support regexp negative lookbehind
|
||||
}
|
||||
return path.match(re);
|
||||
};
|
||||
|
||||
const isDetailPage = (path: string) => {
|
||||
return Object.values(REGEX_DETAIL_PAGES).some((re) => path.match(re));
|
||||
return Object.values(REGEX_DETAIL_PAGES).some((re) =>
|
||||
testDetailPage(path, re)
|
||||
);
|
||||
};
|
||||
|
||||
const isGlobalPage = (path: string) => {
|
||||
|
@ -90,30 +90,29 @@ const getUserTeams = ({
|
||||
};
|
||||
|
||||
const getDefaultTeam = ({
|
||||
userTeams,
|
||||
currentUser,
|
||||
includeAllTeams,
|
||||
includeNoTeam,
|
||||
userTeams,
|
||||
}: {
|
||||
userTeams?: ITeamSummary[];
|
||||
currentUser: IUser | null;
|
||||
includeAllTeams: boolean;
|
||||
includeNoTeam: boolean;
|
||||
userTeams?: ITeamSummary[];
|
||||
}) => {
|
||||
if (!userTeams?.length) {
|
||||
if (!currentUser || !userTeams?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let defaultTeam: ITeamSummary | undefined;
|
||||
if (includeAllTeams) {
|
||||
defaultTeam =
|
||||
userTeams.find((t) => t.id === APP_CONTEXT_ALL_TEAMS_ID) || defaultTeam;
|
||||
} else if (includeNoTeam) {
|
||||
defaultTeam =
|
||||
userTeams.find((t) => t.id === APP_CONTEXT_NO_TEAM_ID) || defaultTeam;
|
||||
} else {
|
||||
defaultTeam =
|
||||
userTeams.find((t) => t.id > APP_CONTEXT_NO_TEAM_ID) || defaultTeam;
|
||||
if (permissions.isOnGlobalTeam(currentUser)) {
|
||||
if (includeAllTeams) {
|
||||
defaultTeam = userTeams.find((t) => t.id === APP_CONTEXT_ALL_TEAMS_ID);
|
||||
}
|
||||
if (!defaultTeam && includeNoTeam) {
|
||||
defaultTeam = userTeams.find((t) => t.id === APP_CONTEXT_NO_TEAM_ID);
|
||||
}
|
||||
}
|
||||
return defaultTeam;
|
||||
return defaultTeam || userTeams.find((t) => t.id > APP_CONTEXT_NO_TEAM_ID);
|
||||
};
|
||||
|
||||
const getTeamIdForApi = ({
|
||||
@ -231,8 +230,14 @@ export const useTeamIdParam = ({
|
||||
);
|
||||
|
||||
const defaultTeam = useMemo(
|
||||
() => getDefaultTeam({ userTeams, includeAllTeams, includeNoTeam }),
|
||||
[includeAllTeams, includeNoTeam, userTeams]
|
||||
() =>
|
||||
getDefaultTeam({
|
||||
currentUser,
|
||||
includeAllTeams,
|
||||
includeNoTeam,
|
||||
userTeams,
|
||||
}),
|
||||
[currentUser, includeAllTeams, includeNoTeam, userTeams]
|
||||
);
|
||||
|
||||
const currentTeam = useMemo(
|
||||
|
@ -1,6 +1,8 @@
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
import React, { useState, useEffect, useContext, useCallback } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { size } from "lodash";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import paths from "router/paths";
|
||||
import { AppContext } from "context/app";
|
||||
@ -8,13 +10,14 @@ import { NotificationContext } from "context/notification";
|
||||
import { RoutingContext } from "context/routing";
|
||||
import { ISSOSettings } from "interfaces/ssoSettings";
|
||||
import local from "utilities/local";
|
||||
import sessionsAPI from "services/entities/sessions";
|
||||
import configAPI from "services/entities/config";
|
||||
import sessionsAPI, { ISSOSettingsResponse } from "services/entities/sessions";
|
||||
import formatErrorResponse from "utilities/format_error_response";
|
||||
|
||||
import AuthenticationFormWrapper from "components/AuthenticationFormWrapper";
|
||||
// @ts-ignore
|
||||
import LoginForm from "components/forms/LoginForm";
|
||||
import { AxiosError } from "axios";
|
||||
import Spinner from "components/Spinner/Spinner";
|
||||
|
||||
interface ILoginPageProps {
|
||||
router: InjectedRouter; // v3
|
||||
@ -48,99 +51,128 @@ const statusMessages: IStatusMessages = {
|
||||
|
||||
const LoginPage = ({ router, location }: ILoginPageProps) => {
|
||||
const {
|
||||
availableTeams,
|
||||
config,
|
||||
currentUser,
|
||||
setAvailableTeams,
|
||||
setConfig,
|
||||
setCurrentUser,
|
||||
setCurrentTeam,
|
||||
} = useContext(AppContext);
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const { redirectLocation } = useContext(RoutingContext);
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [loginVisible, setLoginVisible] = useState(true);
|
||||
const [ssoSettings, setSSOSettings] = useState<ISSOSettings>();
|
||||
const [pageStatus, setPageStatus] = useState<string | null>(
|
||||
new URLSearchParams(location.search).get("status")
|
||||
|
||||
const {
|
||||
data: ssoSettings,
|
||||
isLoading: isLoadingSSOSettings,
|
||||
error: errorSSOSettings,
|
||||
} = useQuery<ISSOSettingsResponse, Error, ISSOSettings>(
|
||||
["ssoSettings"],
|
||||
() => sessionsAPI.ssoSettings(),
|
||||
{
|
||||
enabled: !currentUser,
|
||||
onError: (err) => {
|
||||
console.error(err);
|
||||
},
|
||||
select: (data) => data.settings,
|
||||
}
|
||||
);
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||
|
||||
useEffect(() => {
|
||||
const { DASHBOARD } = paths;
|
||||
const getSSO = async () => {
|
||||
if (
|
||||
availableTeams &&
|
||||
config &&
|
||||
currentUser &&
|
||||
!currentUser.force_password_reset
|
||||
) {
|
||||
router.push(redirectLocation || paths.DASHBOARD);
|
||||
}
|
||||
}, [availableTeams, config, currentUser, redirectLocation, router]);
|
||||
|
||||
useEffect(() => {
|
||||
// this only needs to run once so we can wrap it in useEffect to avoid unneccesary third-party
|
||||
// API calls
|
||||
(async function testGravatarAvailability() {
|
||||
try {
|
||||
const { settings } = await sessionsAPI.ssoSettings();
|
||||
setSSOSettings(settings);
|
||||
const response = await fetch("https://gravatar.com/avatar");
|
||||
if (response.ok) {
|
||||
localStorage.setItem("gravatar_available", "true");
|
||||
} else {
|
||||
localStorage.setItem("gravatar_available", "false");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
localStorage.setItem("gravatar_available", "false");
|
||||
}
|
||||
};
|
||||
})();
|
||||
}, []);
|
||||
|
||||
if (!currentUser) {
|
||||
getSSO();
|
||||
// TODO: Fix this. If renderFlash is added as a dependency it causes infinite re-renders.
|
||||
useEffect(() => {
|
||||
let status = new URLSearchParams(location.search).get("status");
|
||||
status = status && statusMessages[status as keyof IStatusMessages];
|
||||
if (status) {
|
||||
renderFlash("error", status);
|
||||
}
|
||||
}, [location?.search]);
|
||||
|
||||
if (currentUser && !currentUser.force_password_reset) {
|
||||
router?.push(DASHBOARD);
|
||||
}
|
||||
|
||||
if (pageStatus && pageStatus in statusMessages) {
|
||||
renderFlash("error", statusMessages[pageStatus as keyof IStatusMessages]);
|
||||
}
|
||||
}, [router, currentUser]);
|
||||
|
||||
const onChange = () => {
|
||||
const onChange = useCallback(() => {
|
||||
if (size(errors)) {
|
||||
setErrors({});
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}, [errors]);
|
||||
|
||||
const testGravatarAvailability = async () => {
|
||||
try {
|
||||
const response = await fetch("https://gravatar.com/avatar");
|
||||
if (response.ok) {
|
||||
localStorage.setItem("gravatar_available", "true");
|
||||
} else {
|
||||
localStorage.setItem("gravatar_available", "false");
|
||||
const onSubmit = useCallback(
|
||||
async (formData: ILoginData) => {
|
||||
const { DASHBOARD, RESET_PASSWORD } = paths;
|
||||
|
||||
try {
|
||||
const { user, available_teams, token } = await sessionsAPI.create(
|
||||
formData
|
||||
);
|
||||
local.setItem("auth_token", token);
|
||||
|
||||
setLoginVisible(false);
|
||||
setCurrentUser(user);
|
||||
setAvailableTeams(user, available_teams);
|
||||
setCurrentTeam(undefined);
|
||||
|
||||
// Redirect to password reset page if user is forced to reset password.
|
||||
// Any other requests will fail.
|
||||
if (user.force_password_reset) {
|
||||
return router.push(RESET_PASSWORD);
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
const configResponse = await configAPI.loadAll();
|
||||
setConfig(configResponse);
|
||||
}
|
||||
return router.push(redirectLocation || DASHBOARD);
|
||||
} catch (response) {
|
||||
const errorObject = formatErrorResponse(response);
|
||||
setErrors(errorObject);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
localStorage.setItem("gravatar_available", "false");
|
||||
}
|
||||
};
|
||||
},
|
||||
[
|
||||
config,
|
||||
redirectLocation,
|
||||
router,
|
||||
setAvailableTeams,
|
||||
setConfig,
|
||||
setCurrentTeam,
|
||||
setCurrentUser,
|
||||
]
|
||||
);
|
||||
|
||||
testGravatarAvailability();
|
||||
|
||||
const onSubmit = async (formData: ILoginData) => {
|
||||
const { DASHBOARD, RESET_PASSWORD } = paths;
|
||||
|
||||
try {
|
||||
const { user, available_teams, token } = await sessionsAPI.create(
|
||||
formData
|
||||
);
|
||||
local.setItem("auth_token", token);
|
||||
|
||||
setLoginVisible(false);
|
||||
setCurrentUser(user);
|
||||
setAvailableTeams(user, available_teams);
|
||||
setCurrentTeam(undefined);
|
||||
|
||||
// Redirect to password reset page if user is forced to reset password.
|
||||
// Any other requests will fail.
|
||||
if (user.force_password_reset) {
|
||||
return router.push(RESET_PASSWORD);
|
||||
}
|
||||
return router.push(redirectLocation || DASHBOARD);
|
||||
} catch (response) {
|
||||
const errorObject = formatErrorResponse(response);
|
||||
setErrors(errorObject);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const ssoSignOn = async () => {
|
||||
const ssoSignOn = useCallback(async () => {
|
||||
const { DASHBOARD } = paths;
|
||||
let returnToAfterAuth = DASHBOARD;
|
||||
if (redirectLocation != null) {
|
||||
if (redirectLocation !== null) {
|
||||
returnToAfterAuth = redirectLocation;
|
||||
}
|
||||
|
||||
@ -158,7 +190,11 @@ const LoginPage = ({ router, location }: ILoginPageProps) => {
|
||||
setErrors(errorObject);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}, [redirectLocation]);
|
||||
|
||||
if (isLoadingSSOSettings) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthenticationFormWrapper>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ISSOSettings } from "interfaces/ssoSettings";
|
||||
import sendRequest from "services";
|
||||
import endpoints from "utilities/endpoints";
|
||||
import helpers from "utilities/helpers";
|
||||
@ -7,6 +8,10 @@ interface ICreateSessionProps {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface ISSOSettingsResponse {
|
||||
settings: ISSOSettings;
|
||||
}
|
||||
|
||||
export default {
|
||||
create: ({ email, password }: ICreateSessionProps) => {
|
||||
const { LOGIN } = endpoints;
|
||||
@ -30,7 +35,7 @@ export default {
|
||||
const { SSO } = endpoints;
|
||||
return sendRequest("POST", SSO, { relay_url });
|
||||
},
|
||||
ssoSettings: () => {
|
||||
ssoSettings: (): Promise<ISSOSettingsResponse> => {
|
||||
const { SSO } = endpoints;
|
||||
return sendRequest("GET", SSO);
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user