From 4f522088fb37548203302205baa999da16f8ac27 Mon Sep 17 00:00:00 2001
From: Damian Stasik <920747+damianstasik@users.noreply.github.com>
Date: Wed, 28 Aug 2024 20:32:39 +0200
Subject: [PATCH] Improve error handling in the UI

Signed-off-by: Damian Stasik <920747+damianstasik@users.noreply.github.com>
---
 frontend/package-lock.json                   | 13 +++++
 frontend/package.json                        |  1 +
 frontend/src/q.ts                            |  9 +--
 frontend/src/query.ts                        |  3 +
 frontend/src/routes/Error/index.tsx          | 24 ++++----
 frontend/src/routes/Module/query.ts          | 29 +++++-----
 frontend/src/routes/ModuleExample/query.ts   | 10 ++--
 frontend/src/routes/ModuleSubmodule/query.ts | 10 ++--
 frontend/src/routes/Modules/query.ts         | 10 ++--
 frontend/src/routes/Provider/query.ts        | 58 ++++++++------------
 frontend/src/routes/Providers/query.ts       | 10 ++--
 frontend/src/utils/errors.tsx                |  9 +++
 12 files changed, 93 insertions(+), 93 deletions(-)

diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index f55fe4cb..d5c466e0 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -22,6 +22,7 @@
         "eslint-plugin-react-hooks": "^5.1.0-rc.0",
         "eslint-plugin-react-refresh": "^0.4.11",
         "globals": "^15.9.0",
+        "ky": "^1.7.1",
         "lunr": "^2.3.9",
         "openapi-typescript": "^5.4.2",
         "postcss": "^8.4.41",
@@ -3559,6 +3560,18 @@
         "json-buffer": "3.0.1"
       }
     },
+    "node_modules/ky": {
+      "version": "1.7.1",
+      "resolved": "https://registry.npmjs.org/ky/-/ky-1.7.1.tgz",
+      "integrity": "sha512-KJ/IXXkFhTDqxcN8wKqMXk1/UoOpc0UnOB6H7QcqlPInh/M2B5Mlj+i9exez1w4RSwJhNFmHiUDPriAYFwb5VA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/ky?sponsor=1"
+      }
+    },
     "node_modules/levn": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 9b6c1552..979af420 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -27,6 +27,7 @@
     "eslint-plugin-react-hooks": "^5.1.0-rc.0",
     "eslint-plugin-react-refresh": "^0.4.11",
     "globals": "^15.9.0",
+    "ky": "^1.7.1",
     "lunr": "^2.3.9",
     "openapi-typescript": "^5.4.2",
     "postcss": "^8.4.41",
diff --git a/frontend/src/q.ts b/frontend/src/q.ts
index ee96ad85..b957548e 100644
--- a/frontend/src/q.ts
+++ b/frontend/src/q.ts
@@ -1,16 +1,13 @@
 import { queryOptions } from "@tanstack/react-query";
 import lunr from "lunr";
+import { api } from "./query";
 
 export const getSearchIndexQuery = () =>
   queryOptions({
     queryKey: ["search-index"],
     queryFn: async () => {
-      const response = await fetch(
-        `${import.meta.env.VITE_DATA_API_URL}/search.json`,
-      );
+      const data = await api(`search.json`).json();
 
-      const res = await response.json();
-
-      return lunr.Index.load(res);
+      return lunr.Index.load(data);
     },
   });
diff --git a/frontend/src/query.ts b/frontend/src/query.ts
index 9513ade6..d2cf541a 100644
--- a/frontend/src/query.ts
+++ b/frontend/src/query.ts
@@ -1,4 +1,5 @@
 import { QueryClient } from "@tanstack/react-query";
+import ky from "ky";
 
 export const queryClient = new QueryClient({
   defaultOptions: {
@@ -7,3 +8,5 @@ export const queryClient = new QueryClient({
     },
   },
 });
+
+export const api = ky.create({ prefixUrl: import.meta.env.VITE_DATA_API_URL });
diff --git a/frontend/src/routes/Error/index.tsx b/frontend/src/routes/Error/index.tsx
index 42f22b0e..8bd13019 100644
--- a/frontend/src/routes/Error/index.tsx
+++ b/frontend/src/routes/Error/index.tsx
@@ -1,21 +1,19 @@
 import { useRouteError } from "react-router-dom";
-import { Header } from "../../components/Header";
-import { Paragraph } from "../../components/Paragraph";
-import PatternBg from "../../components/PatternBg";
-import { NotFoundPageError } from "@/utils/errors";
+import { Header } from "@/components/Header";
+import { Paragraph } from "@/components/Paragraph";
+import PatternBg from "@/components/PatternBg";
+import { is404Error } from "@/utils/errors";
 
 export function Error() {
   const routeError = useRouteError() as Error;
 
-  const title =
-    routeError instanceof NotFoundPageError
-      ? "Page Not Found"
-      : "An Error Occurred";
+  const is404 = is404Error(routeError);
 
-  const message =
-    routeError instanceof NotFoundPageError
-      ? "The page you are looking for does not exist."
-      : "We're sorry, but an unexpected error occurred. Please try again later.";
+  const title = is404 ? "Page Not Found" : "An Error Occurred";
+
+  const message = is404
+    ? "The page you are looking for does not exist."
+    : "We're sorry, but an unexpected error occurred. Please try again later.";
 
   return (
     <>
@@ -24,7 +22,7 @@ export function Error() {
       <main className="container m-auto flex flex-col items-center gap-8 text-center">
         <h2 className="text-6xl font-bold">{title}</h2>
         <Paragraph className="text-balance">{message}</Paragraph>
-        {!!routeError.message && (
+        {import.meta.env.DEV && !!routeError.message && (
           <pre className="text-balance">{routeError.message}</pre>
         )}
       </main>
diff --git a/frontend/src/routes/Module/query.ts b/frontend/src/routes/Module/query.ts
index f806d264..0bd7726e 100644
--- a/frontend/src/routes/Module/query.ts
+++ b/frontend/src/routes/Module/query.ts
@@ -1,4 +1,5 @@
 import { definitions } from "@/api";
+import { api } from "@/query";
 import { queryOptions } from "@tanstack/react-query";
 
 export const getModuleVersionDataQuery = (
@@ -10,13 +11,11 @@ export const getModuleVersionDataQuery = (
   return queryOptions({
     queryKey: ["module-version", namespace, name, target, version],
     queryFn: async () => {
-      const response = await fetch(
-        `${import.meta.env.VITE_DATA_API_URL}/modules/${namespace}/${name}/${target}/${version}/index.json`,
-      );
+      const data = await api(
+        `modules/${namespace}/${name}/${target}/${version}/index.json`,
+      ).json<definitions["ModuleVersion"]>();
 
-      const data = await response.json();
-
-      return data as definitions["ModuleVersion"];
+      return data;
     },
   });
 };
@@ -29,13 +28,11 @@ export const getModuleDataQuery = (
   return queryOptions({
     queryKey: ["module", namespace, name, target],
     queryFn: async () => {
-      const response = await fetch(
-        `${import.meta.env.VITE_DATA_API_URL}/modules/${namespace}/${name}/${target}/index.json`,
-      );
-
-      const data = await response.json();
+      const data = await api(
+        `modules/${namespace}/${name}/${target}/index.json`,
+      ).json<definitions["Module"]>();
 
-      return data as definitions["Module"];
+      return data;
     },
   });
 };
@@ -49,11 +46,11 @@ export const getModuleReadmeQuery = (
   return queryOptions({
     queryKey: ["module-readme", namespace, name, target, version],
     queryFn: async () => {
-      const response = await fetch(
-        `${import.meta.env.VITE_DATA_API_URL}/modules/${namespace}/${name}/${target}/${version}/README.md`,
-      );
+      const data = await api(
+        `modules/${namespace}/${name}/${target}/${version}/README.md`,
+      ).text();
 
-      return response.text();
+      return data;
     },
   });
 };
diff --git a/frontend/src/routes/ModuleExample/query.ts b/frontend/src/routes/ModuleExample/query.ts
index c6597ce2..d0ef72cc 100644
--- a/frontend/src/routes/ModuleExample/query.ts
+++ b/frontend/src/routes/ModuleExample/query.ts
@@ -1,4 +1,4 @@
-import { queryClient } from "@/query";
+import { api, queryClient } from "@/query";
 import { queryOptions } from "@tanstack/react-query";
 import { getModuleVersionDataQuery } from "../Module/query";
 import { NotFoundPageError } from "@/utils/errors";
@@ -20,11 +20,11 @@ export const getModuleExampleReadmeQuery = (
       example,
     ],
     queryFn: async () => {
-      const response = await fetch(
-        `${import.meta.env.VITE_DATA_API_URL}/modules/${namespace}/${name}/${target}/${version}/examples/${example}/README.md`,
-      );
+      const data = await api(
+        `modules/${namespace}/${name}/${target}/${version}/examples/${example}/README.md`,
+      ).text();
 
-      return response.text();
+      return data;
     },
   });
 };
diff --git a/frontend/src/routes/ModuleSubmodule/query.ts b/frontend/src/routes/ModuleSubmodule/query.ts
index 3418effe..e51cd116 100644
--- a/frontend/src/routes/ModuleSubmodule/query.ts
+++ b/frontend/src/routes/ModuleSubmodule/query.ts
@@ -1,4 +1,4 @@
-import { queryClient } from "@/query";
+import { api, queryClient } from "@/query";
 import { queryOptions } from "@tanstack/react-query";
 import { getModuleVersionDataQuery } from "../Module/query";
 import { NotFoundPageError } from "@/utils/errors";
@@ -20,11 +20,11 @@ export const getModuleSubmoduleReadmeQuery = (
       submodule,
     ],
     queryFn: async () => {
-      const response = await fetch(
-        `${import.meta.env.VITE_DATA_API_URL}/modules/${namespace}/${name}/${target}/${version}/modules/${submodule}/README.md`,
-      );
+      const data = await api(
+        `modules/${namespace}/${name}/${target}/${version}/modules/${submodule}/README.md`,
+      ).text();
 
-      return response.text();
+      return data;
     },
   });
 };
diff --git a/frontend/src/routes/Modules/query.ts b/frontend/src/routes/Modules/query.ts
index 6fce1acf..43de0374 100644
--- a/frontend/src/routes/Modules/query.ts
+++ b/frontend/src/routes/Modules/query.ts
@@ -1,16 +1,14 @@
 import { queryOptions } from "@tanstack/react-query";
 import { definitions } from "@/api";
+import { api } from "@/query";
 
 export const getModulesQuery = () =>
   queryOptions({
     queryKey: ["modules"],
     queryFn: async () => {
-      const response = await fetch(
-        `${import.meta.env.VITE_DATA_API_URL}/modules/index.json`,
-      );
+      const data =
+        await api(`modules/index.json`).json<definitions["ModuleList"]>();
 
-      const res = await response.json();
-
-      return res.modules as definitions["ModuleList"]["modules"];
+      return data.modules;
     },
   });
diff --git a/frontend/src/routes/Provider/query.ts b/frontend/src/routes/Provider/query.ts
index e87e8098..d12e4396 100644
--- a/frontend/src/routes/Provider/query.ts
+++ b/frontend/src/routes/Provider/query.ts
@@ -1,26 +1,21 @@
 import { definitions } from "@/api";
-import { queryOptions, skipToken } from "@tanstack/react-query";
+import { api } from "@/query";
+import { queryOptions } from "@tanstack/react-query";
 
 export const getProviderVersionDataQuery = (
   namespace: string | undefined,
   provider: string | undefined,
   version: string | undefined,
 ) => {
-  const hasParams = namespace && provider && version;
-
   return queryOptions({
     queryKey: ["provider-version", namespace, provider, version],
-    queryFn: hasParams
-      ? async () => {
-          const response = await fetch(
-            `${import.meta.env.VITE_DATA_API_URL}/providers/${namespace}/${provider}/${version}/index.json`,
-          );
-
-          const data = await response.json();
+    queryFn: async () => {
+      const data = await api(
+        `providers/${namespace}/${provider}/${version}/index.json`,
+      ).json<definitions["ProviderVersion"]>();
 
-          return data as definitions["ProviderVersion"];
-        }
-      : skipToken,
+      return data;
+    },
   });
 };
 
@@ -35,18 +30,14 @@ export const getProviderDocsQuery = (
   return queryOptions({
     queryKey: ["provider-doc", namespace, provider, type, name, lang, version],
     queryFn: async () => {
-      try {
-        const urlBase = `${import.meta.env.VITE_DATA_API_URL}/providers/${namespace}/${provider}/${version}`;
-        const requestURL =
-          type === undefined && name === undefined
-            ? `${urlBase}/index.md`
-            : `${urlBase}/${lang ? `cdktf/${lang}/` : ""}${type}/${name}.md`;
-
-        const response = await fetch(requestURL);
-        return response.text();
-      } catch {
-        return "";
-      }
+      const urlBase = `providers/${namespace}/${provider}/${version}`;
+      const requestURL =
+        type === undefined && name === undefined
+          ? `${urlBase}/index.md`
+          : `${urlBase}/${lang ? `cdktf/${lang}/` : ""}${type}/${name}.md`;
+
+      const response = await api(requestURL).text();
+      return response;
     },
   });
 };
@@ -57,17 +48,12 @@ export const getProviderDataQuery = (
 ) => {
   return queryOptions({
     queryKey: ["provider", namespace, provider],
-    queryFn:
-      namespace && provider
-        ? async () => {
-            const response = await fetch(
-              `${import.meta.env.VITE_DATA_API_URL}/providers/${namespace}/${provider}/index.json`,
-            );
-
-            const data = await response.json();
+    queryFn: async () => {
+      const data = await api(
+        `providers/${namespace}/${provider}/index.json`,
+      ).json<definitions["Provider"]>();
 
-            return data as definitions["Provider"];
-          }
-        : skipToken,
+      return data;
+    },
   });
 };
diff --git a/frontend/src/routes/Providers/query.ts b/frontend/src/routes/Providers/query.ts
index e4c3fa48..cb38bdb3 100644
--- a/frontend/src/routes/Providers/query.ts
+++ b/frontend/src/routes/Providers/query.ts
@@ -1,16 +1,14 @@
 import { queryOptions } from "@tanstack/react-query";
 import { definitions } from "@/api";
+import { api } from "@/query";
 
 export const getProvidersQuery = () =>
   queryOptions({
     queryKey: ["providers"],
     queryFn: async () => {
-      const response = await fetch(
-        `${import.meta.env.VITE_DATA_API_URL}/providers/index.json`,
-      );
+      const data =
+        await api(`providers/index.json`).json<definitions["ProviderList"]>();
 
-      const res = await response.json();
-
-      return res.providers as definitions["ProviderList"]["providers"];
+      return data.providers;
     },
   });
diff --git a/frontend/src/utils/errors.tsx b/frontend/src/utils/errors.tsx
index f8eca856..60d9fd2b 100644
--- a/frontend/src/utils/errors.tsx
+++ b/frontend/src/utils/errors.tsx
@@ -1 +1,10 @@
+import { HTTPError } from "ky";
+
 export class NotFoundPageError extends Error {}
+
+export function is404Error(error: unknown) {
+  return (
+    error instanceof NotFoundPageError ||
+    (error instanceof HTTPError && error.response.status === 404)
+  );
+}