Skip to content

Commit

Permalink
Added ConfigProvider to ensure config data is always available (#5465)
Browse files Browse the repository at this point in the history
  • Loading branch information
bilalabbad authored Jan 15, 2025
1 parent 1759e1b commit a08a438
Show file tree
Hide file tree
Showing 24 changed files with 138 additions and 165 deletions.
26 changes: 15 additions & 11 deletions frontend/app/cypress/support/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { ReactRouter6Adapter } from "use-query-params/adapters/react-router-6";
import "./commands";

import "../../src/app/styles/index.css";
import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "../../src/shared/api/rest/client";

// Alternatively you can use CommonJS syntax:
// require('./commands')
Expand Down Expand Up @@ -54,17 +56,19 @@ Cypress.Commands.add("mount", (component, options = {}) => {

const wrapped = (
<React.StrictMode>
<MemoryRouter {...routerProps} basename="/">
<QueryParamProvider
adapter={ReactRouter6Adapter}
options={{
searchStringToObject: queryString.parse,
objectToSearchString: queryString.stringify,
}}
>
{component}
</QueryParamProvider>
</MemoryRouter>
<QueryClientProvider client={queryClient}>
<MemoryRouter {...routerProps} basename="/">
<QueryParamProvider
adapter={ReactRouter6Adapter}
options={{
searchStringToObject: queryString.parse,
objectToSearchString: queryString.stringify,
}}
>
{component}
</QueryParamProvider>
</MemoryRouter>
</QueryClientProvider>
</React.StrictMode>
);

Expand Down
33 changes: 18 additions & 15 deletions frontend/app/src/app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { ApolloProvider } from "@apollo/client";
import { addCollection } from "@iconify-icon/react";
import mdiIcons from "@iconify-json/mdi/icons.json";
import { QueryClientProvider } from "@tanstack/react-query";
import { Provider } from "jotai";
import { ErrorBoundary } from "react-error-boundary";
import { RouterProvider } from "react-router-dom";
import { Slide, ToastContainer } from "react-toastify";

import { TanStackQueryDevtools } from "@/app/devtools";
import { router } from "@/app/router";
import { AuthProvider } from "@/entities/authentication/ui/useAuth";
import { ConfigProvider } from "@/entities/config/config-provider";
import graphqlClient from "@/shared/api/graphql/graphqlClientApollo";
import { queryClient } from "@/shared/api/rest/client";
import ErrorFallback from "@/shared/components/errors/error-fallback";
import { store } from "@/shared/stores";
import { ApolloProvider } from "@apollo/client";
import { addCollection } from "@iconify-icon/react";
import mdiIcons from "@iconify-json/mdi/icons.json";
import { QueryClientProvider } from "@tanstack/react-query";

import "@/app/styles/index.css";
import "react-toastify/dist/ReactToastify.css";
import { TanStackQueryDevtools } from "@/app/devtools";
import { queryClient } from "@/shared/api/rest/client";

addCollection(mdiIcons);

Expand All @@ -27,15 +28,17 @@ export function App() {
<AuthProvider>
<QueryClientProvider client={queryClient}>
<ApolloProvider client={graphqlClient}>
<ToastContainer
hideProgressBar={true}
transition={Slide}
autoClose={5000}
closeOnClick={false}
newestOnTop
position="bottom-right"
/>
<RouterProvider router={router} />
<ConfigProvider>
<ToastContainer
hideProgressBar={true}
transition={Slide}
autoClose={5000}
closeOnClick={false}
newestOnTop
position="bottom-right"
/>
<RouterProvider router={router} />
</ConfigProvider>
</ApolloProvider>
<TanStackQueryDevtools buttonPosition="bottom-left" />
</QueryClientProvider>
Expand Down
59 changes: 0 additions & 59 deletions frontend/app/src/app/root.tsx

This file was deleted.

5 changes: 1 addition & 4 deletions frontend/app/src/app/router.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Root } from "@/app/root";
import { NODE_OBJECT, PROPOSED_CHANGES_OBJECT } from "@/config/constants";
import { RequireAuth } from "@/entities/authentication/ui/useAuth";
import { constructPathForIpam } from "@/entities/ipam/common/utils";
Expand All @@ -21,9 +20,7 @@ export const router = createBrowserRouter([
objectToSearchString: queryString.stringify,
}}
>
<Root>
<Outlet />
</Root>
<Outlet />
</QueryParamProvider>
),
children: [
Expand Down
44 changes: 0 additions & 44 deletions frontend/app/src/config/config.atom.ts

This file was deleted.

3 changes: 0 additions & 3 deletions frontend/app/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export const CONFIG = {
branch
? `${INFRAHUB_API_SERVER_URL}/api/schema?branch=${branch}`
: `${INFRAHUB_API_SERVER_URL}/api/schema`,
CONFIG_URL: `${INFRAHUB_API_SERVER_URL}/api/config`,
SEARCH_URL: (query: string, limit: number = 3) =>
`${INFRAHUB_API_SERVER_URL}/api/search/docs?query=${query}&limit=${limit}`,
INFO_URL: `${INFRAHUB_API_SERVER_URL}/api/info`,
Expand All @@ -50,6 +49,4 @@ export const CONFIG = {
FILES_CONTENT_URL: (repositoryId: string, location: string) =>
`${INFRAHUB_API_SERVER_URL}/api/file/${repositoryId}/${encodeURIComponent(location)}`,
STORAGE_DETAILS_URL: (id: string) => `${INFRAHUB_API_SERVER_URL}/api/storage/object/${id}`,
MENU_URL: (branch?: string) =>
`${INFRAHUB_API_SERVER_URL}/api/menu${branch ? `?branch=${branch}` : ""}`,
};
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { INFRAHUB_API_SERVER_URL } from "@/config/config";
import { Provider } from "@/config/config.atom";
import { SSOProvider } from "@/entities/config/types";
import { classNames } from "@/shared/utils/common";
import { Icon } from "@iconify-icon/react";
import { useLocation } from "react-router-dom";

export interface LoginWithSSOButtonsProps {
className?: string;
providers: Array<Provider>;
providers: Array<SSOProvider>;
}

export const LoginWithSSOButtons = ({ className, providers }: LoginWithSSOButtonsProps) => {
Expand All @@ -30,7 +30,7 @@ export const LoginWithSSOButtons = ({ className, providers }: LoginWithSSOButton
export const ProviderButton = ({
provider,
redirectTo = "/",
}: { provider: Provider; redirectTo?: string }) => {
}: { provider: SSOProvider; redirectTo?: string }) => {
return (
<a
className="h-9 px-4 py-2 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium disabled:opacity-60 disabled:cursor-not-allowed border bg-custom-white shadow-sm hover:bg-gray-100"
Expand Down
7 changes: 3 additions & 4 deletions frontend/app/src/entities/authentication/ui/login.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { configState } from "@/config/config.atom";
import { LoginWithSSOButtons } from "@/entities/authentication/ui/login-sso-buttons";
import { useAuth } from "@/entities/authentication/ui/useAuth";
import { useConfig } from "@/entities/config/get-config.query";
import { Button } from "@/shared/components/buttons/button-primitive";
import InputField from "@/shared/components/form/fields/input.field";
import PasswordInputField from "@/shared/components/form/fields/password-input.field";
import { isRequired } from "@/shared/components/form/utils/validation";
import { Form, FormSubmit } from "@/shared/components/ui/form";
import { classNames } from "@/shared/utils/common";
import { useAtomValue } from "jotai";
import { useState } from "react";

export const Login = () => {
const config = useAtomValue(configState);
const { data: config } = useConfig();
const [displaySSO, setDisplaySSO] = useState(true);

if (config && config.sso.enabled && config.sso.providers.length > 0) {
if (config && config.sso.enabled && config.sso.providers && config.sso.providers.length > 0) {
return displaySSO ? (
<>
<LoginWithSSOButtons providers={config.sso.providers} className="animate-in fade-in" />
Expand Down
7 changes: 3 additions & 4 deletions frontend/app/src/entities/authentication/ui/useAuth.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { CONFIG } from "@/config/config";
import { configState } from "@/config/config.atom";
import { REFRESH_TOKEN_KEY } from "@/config/constants";
import { ACCESS_TOKEN_KEY } from "@/config/localStorage";
import { useConfig } from "@/entities/config/get-config.query";
import graphqlClient from "@/shared/api/graphql/graphqlClientApollo";
import { fetchUrl } from "@/shared/api/rest/fetch";
import { components } from "@/shared/api/rest/types.generated";
import { ALERT_TYPES, Alert } from "@/shared/components/ui/alert";
import { parseJwt } from "@/shared/utils/common";
import { ObservableQuery } from "@apollo/client";
import { useAtom } from "jotai/index";
import { ReactElement, ReactNode, createContext, useContext, useState } from "react";
import { Navigate, useLocation } from "react-router-dom";
import { toast } from "react-toastify";
Expand Down Expand Up @@ -173,11 +172,11 @@ export function useAuth() {
}

export function RequireAuth({ children }: { children: ReactElement }) {
const [config] = useAtom(configState);
const { data: config } = useConfig();
const { isAuthenticated } = useAuth();
const location = useLocation();

if (isAuthenticated || config?.main?.allow_anonymous_access) return children;
if (isAuthenticated || config.main.allow_anonymous_access) return children;

// Redirect them to the /login page, but save the current location they were
// trying to go to when they were redirected. This allows us to send them
Expand Down
19 changes: 19 additions & 0 deletions frontend/app/src/entities/config/config-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { getConfigQueryOptions } from "@/entities/config/get-config.query";
import ErrorScreen from "@/shared/components/errors/error-screen";
import { InfrahubLoading } from "@/shared/components/loading/infrahub-loading";
import { useQuery } from "@tanstack/react-query";
import React from "react";

export const ConfigProvider = ({ children }: { children: React.ReactNode }) => {
const { isPending, error } = useQuery(getConfigQueryOptions());

if (isPending) {
return <InfrahubLoading>Loading config...</InfrahubLoading>;
}

if (error) {
return <ErrorScreen message={error.message} />;
}

return children;
};
13 changes: 13 additions & 0 deletions frontend/app/src/entities/config/get-config.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { getConfig } from "@/entities/config/get-config";
import { queryOptions, useSuspenseQuery } from "@tanstack/react-query";

export const getConfigQueryOptions = () => {
return queryOptions({
queryKey: ["config"],
queryFn: getConfig,
});
};

export const useConfig = () => {
return useSuspenseQuery(getConfigQueryOptions());
};
12 changes: 12 additions & 0 deletions frontend/app/src/entities/config/get-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ConfigAPI } from "@/entities/config/types";
import { apiClient } from "@/shared/api/rest/client";

export type GetConfig = () => Promise<ConfigAPI>;

export const getConfig: GetConfig = async () => {
const { data, error } = await apiClient.GET("/api/config");

if (error) throw error;

return data;
};
5 changes: 5 additions & 0 deletions frontend/app/src/entities/config/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { components } from "@/shared/api/rest/types.generated";

export type ConfigAPI = components["schemas"]["ConfigAPI"];

export type SSOProvider = components["schemas"]["SSOProviderInfo"];
6 changes: 3 additions & 3 deletions frontend/app/src/entities/permission/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { configState } from "@/config/config.atom";
import { getConfigQueryOptions } from "@/entities/config/get-config.query";
import {
Permission,
PermissionAction,
PermissionData,
PermissionDecision,
PermissionDecisionData,
} from "@/entities/permission/types";
import { store } from "@/shared/stores";
import { queryClient } from "@/shared/api/rest/client";
import { warnUnexpectedType } from "@/shared/utils/common";

const getMessage = (action: string, decision?: PermissionDecisionData): string => {
Expand All @@ -31,7 +31,7 @@ const getMessage = (action: string, decision?: PermissionDecisionData): string =
export function getPermission(permission?: Array<{ node: PermissionData }>): Permission {
if (!Array.isArray(permission)) return PERMISSION_ALLOW_ALL;

const config = store.get(configState);
const config = queryClient.getQueryData(getConfigQueryOptions().queryKey);

const createPermissionAction = (action: PermissionAction): PermissionDecision => {
if (action === "view" && config?.main.allow_anonymous_access) return { isAllowed: true };
Expand Down
Loading

0 comments on commit a08a438

Please sign in to comment.