Fix unreleased UI bugs in login page, top nav, and default team (#10928)

This commit is contained in:
gillespi314 2023-04-03 10:13:57 -05:00 committed by GitHub
parent 23fe4bc718
commit d3bbed34ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 145 additions and 89 deletions

View File

@ -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) => {

View File

@ -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(

View File

@ -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>

View File

@ -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);
},