diff --git a/src/Panel.tsx b/src/Panel.tsx index 864b0b50..d1c4bb3d 100644 --- a/src/Panel.tsx +++ b/src/Panel.tsx @@ -7,6 +7,7 @@ import { Sections } from "./components/layout"; import { ADDON_ID, GIT_INFO, + GIT_INFO_ERROR, IS_OUTDATED, LOCAL_BUILD_PROGRESS, PANEL_ID, @@ -14,6 +15,7 @@ import { } from "./constants"; import { Project } from "./gql/graphql"; import { Authentication } from "./screens/Authentication/Authentication"; +import { GitNotFound } from "./screens/GitNotFound/GitNotFound"; import { LinkedProject } from "./screens/LinkProject/LinkedProject"; import { LinkingProjectFailed } from "./screens/LinkProject/LinkingProjectFailed"; import { LinkProject } from "./screens/LinkProject/LinkProject"; @@ -33,6 +35,7 @@ export const Panel = ({ active, api }: PanelProps) => { const { storyId } = useStorybookState(); const [gitInfo] = useAddonState(GIT_INFO); + const [gitInfoError] = useAddonState(GIT_INFO_ERROR); const [localBuildProgress] = useAddonState(LOCAL_BUILD_PROGRESS); const [, setOutdated] = useAddonState(IS_OUTDATED); const emit = useChannel({}); @@ -55,6 +58,16 @@ export const Panel = ({ active, api }: PanelProps) => { // If the user creates a project in a dialog (either during login or later, it get set here) const [createdProjectId, setCreatedProjectId] = useState(); + if (gitInfoError) { + return ( + + + + ); + } + // Render the Authentication flow if the user is not signed in. if (!accessToken) { return ( diff --git a/src/SidebarTop.tsx b/src/SidebarTop.tsx index 821d5923..125de665 100644 --- a/src/SidebarTop.tsx +++ b/src/SidebarTop.tsx @@ -4,7 +4,13 @@ import pluralize from "pluralize"; import React, { useEffect, useRef } from "react"; import { SidebarTopButton } from "./components/SidebarTopButton"; -import { ADDON_ID, IS_OUTDATED, LOCAL_BUILD_PROGRESS, START_BUILD } from "./constants"; +import { + ADDON_ID, + GIT_INFO_ERROR, + IS_OUTDATED, + LOCAL_BUILD_PROGRESS, + START_BUILD, +} from "./constants"; import { LocalBuildProgress } from "./types"; import { useAddonState } from "./useAddonState/manager"; import { useAccessToken } from "./utils/graphQLClient"; @@ -25,6 +31,8 @@ export const SidebarTop = ({ api }: SidebarTopProps) => { const [localBuildProgress] = useAddonState(LOCAL_BUILD_PROGRESS); const isRunning = !!localBuildProgress && localBuildProgress.currentStep !== "complete"; + const [gitInfoError] = useAddonState(GIT_INFO_ERROR); + const lastStep = useRef(localBuildProgress?.currentStep); useEffect(() => { if (localBuildProgress?.currentStep === lastStep.current) return; @@ -95,7 +103,7 @@ export const SidebarTop = ({ api }: SidebarTopProps) => { const emit = useChannel({}); const startBuild = () => emit(START_BUILD); - if (!projectId || isLoggedIn === false) { + if (!projectId || isLoggedIn === false || gitInfoError) { return null; } diff --git a/src/constants.ts b/src/constants.ts index f4a3e51b..d559ff6f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -12,6 +12,7 @@ export const ACCESS_TOKEN_KEY = `${ADDON_ID}/access-token/${CHROMATIC_BASE_URL}` export const DEV_BUILD_ID_KEY = `${ADDON_ID}/dev-build-id`; export const GIT_INFO = `${ADDON_ID}/gitInfo`; +export const GIT_INFO_ERROR = `${ADDON_ID}/gitInfoError`; export const PROJECT_INFO = `${ADDON_ID}/projectInfo`; export const IS_OUTDATED = `${ADDON_ID}/isOutdated`; export const START_BUILD = `${ADDON_ID}/startBuild`; diff --git a/src/index.ts b/src/index.ts index ba6dafed..249844d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { getConfiguration, getGitInfo, GitInfo } from "chromatic/node"; import { CHROMATIC_BASE_URL, GIT_INFO, + GIT_INFO_ERROR, LOCAL_BUILD_PROGRESS, PROJECT_INFO, START_BUILD, @@ -26,17 +27,30 @@ function managerEntries(entry: string[] = []) { // Uses a recursive setTimeout instead of setInterval to avoid overlapping async calls. const observeGitInfo = async ( interval: number, - callback: (info: GitInfo, prevInfo: GitInfo) => void + callback: (info: GitInfo, prevInfo?: GitInfo) => void, + errorCallback: (e: Error) => void ) => { - let prev: GitInfo; + let prev: GitInfo | undefined; + let prevError: Error | undefined; let timer: NodeJS.Timeout | undefined; const act = async () => { - const gitInfo = await getGitInfo(); - if (Object.entries(gitInfo).some(([key, value]) => prev?.[key as keyof GitInfo] !== value)) { - callback(gitInfo, prev); + try { + const gitInfo = await getGitInfo(); + if (Object.entries(gitInfo).some(([key, value]) => prev?.[key as keyof GitInfo] !== value)) { + callback(gitInfo, prev); + } + prev = gitInfo; + prevError = undefined; + timer = setTimeout(act, interval); + } catch (e: any) { + if (prevError?.message !== e.message) { + console.error(`Failed to fetch git info, with error:\n${e}`); + errorCallback(e); + } + prev = undefined; + prevError = e; + timer = setTimeout(act, interval); } - prev = gitInfo; - timer = setTimeout(act, interval); }; act(); @@ -99,9 +113,20 @@ async function serverChannel( // eslint-disable-next-line react-hooks/rules-of-hooks const gitInfoState = useAddonState(channel, GIT_INFO); - observeGitInfo(5000, (info) => { - gitInfoState.value = info; - }); + + // eslint-disable-next-line react-hooks/rules-of-hooks + const gitInfoError = useAddonState(channel, GIT_INFO_ERROR); + + observeGitInfo( + 5000, + (info) => { + gitInfoError.value = undefined; + gitInfoState.value = info; + }, + (error: Error) => { + gitInfoError.value = error; + } + ); return channel; } diff --git a/src/screens/GitNotFound/GitNotFound.stories.tsx b/src/screens/GitNotFound/GitNotFound.stories.tsx new file mode 100644 index 00000000..0b9371bc --- /dev/null +++ b/src/screens/GitNotFound/GitNotFound.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import React from "react"; + +import { storyWrapper } from "../../utils/graphQLClient"; +import { GitNotFound } from "./GitNotFound"; + +const meta = { + component: GitNotFound, + decorators: [storyWrapper], + args: { + gitInfoError: new Error("Git info not found"), + }, +} satisfies Meta; + +export const Default = {} satisfies StoryObj; + +export default meta; diff --git a/src/screens/GitNotFound/GitNotFound.tsx b/src/screens/GitNotFound/GitNotFound.tsx new file mode 100644 index 00000000..176c7d33 --- /dev/null +++ b/src/screens/GitNotFound/GitNotFound.tsx @@ -0,0 +1,58 @@ +import { Icon, Link } from "@storybook/design-system"; +import { styled } from "@storybook/theming"; +import React from "react"; + +import { Container } from "../../components/Container"; +import { Heading } from "../../components/Heading"; +import { VisualTestsIcon } from "../../components/icons/VisualTestsIcon"; +import { Section } from "../../components/layout"; +import { Stack } from "../../components/Stack"; +import { Text } from "../../components/Text"; + +interface GitNotFoundProps { + gitInfoError: Error; +} + +const InfoSection = styled(Section)(({ theme }) => ({ + display: "flex", + flexDirection: "row", + alignItems: "center", + borderRadius: theme.appBorderRadius, + background: theme.color.lightest, + padding: 15, + flex: 1, + boxShadow: `0px 1px 3px 0px rgba(0, 0, 0, 0.10), 0px 2px 5px 0px rgba(0, 0, 0, 0.05)`, +})); + +const InfoSectionText = styled(Text)(({ theme }) => ({ + marginLeft: 14, + flex: 1, + textAlign: "left", + color: theme.color.darker, +})); + +export const GitNotFound = ({ gitInfoError }: GitNotFoundProps) => ( + + +
+ + Visual tests + + Catch bugs in UI appearance automatically. Compare image snapshots to detect visual + changes. + +
+ + + + Git not detected +
+ This addon requires Git to associate test results with commits and branches. +
+
+ + Visual tests requirements + +
+
+);