From c0130eb302b16ec19d897946be42b58ce64b94c7 Mon Sep 17 00:00:00 2001 From: Alex Stephen Date: Tue, 2 Dec 2025 18:29:31 -0800 Subject: [PATCH] Bunch of settings changes --- package-lock.json | 10 ++ package.json | 1 + src/App.tsx | 5 +- src/app/sidebar/app-sidebar.tsx | 75 +++--------- src/app/spec_specifier/page.tsx | 17 +-- src/components/oas-selector.tsx | 107 ---------------- src/components/settings.tsx | 208 ++++++++++++++++++++++++++++++++ src/state/store.ts | 71 ++++++++++- 8 files changed, 311 insertions(+), 183 deletions(-) delete mode 100644 src/components/oas-selector.tsx create mode 100644 src/components/settings.tsx diff --git a/package-lock.json b/package-lock.json index 081f73c..304d84e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "react-hook-form": "^7.53.2", "react-redux": "^9.2.0", "react-router-dom": "^6.28.0", + "redux-persist": "^6.0.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8" @@ -6977,6 +6978,15 @@ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "license": "MIT" }, + "node_modules/redux-persist": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", + "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", + "license": "MIT", + "peerDependencies": { + "redux": ">4.0.0" + } + }, "node_modules/redux-thunk": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", diff --git a/package.json b/package.json index dfe5bcb..d3e3fd8 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "react-hook-form": "^7.53.2", "react-redux": "^9.2.0", "react-router-dom": "^6.28.0", + "redux-persist": "^6.0.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8" diff --git a/src/App.tsx b/src/App.tsx index 3f9af3e..96d30bc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,8 +7,9 @@ import CreateForm from "./components/form/form"; import InfoPage from "./app/explorer/info"; import UpdatePage from "./app/explorer/update_form"; -import { selectResources, store } from './state/store'; +import { selectResources, store, persistor } from './state/store'; import { Provider } from 'react-redux' +import { PersistGate } from 'redux-persist/integration/react' import { useAppSelector } from "./hooks/store"; function transformUrlForRouter(url: string): string { @@ -52,7 +53,9 @@ function createRoutes(resources: ResourceSchema[]): RouteObject[] { function App() { return ( + + ); } diff --git a/src/app/sidebar/app-sidebar.tsx b/src/app/sidebar/app-sidebar.tsx index bc108fb..95cbdf8 100644 --- a/src/app/sidebar/app-sidebar.tsx +++ b/src/app/sidebar/app-sidebar.tsx @@ -2,17 +2,17 @@ import * as React from "react"; import { Sidebar, SidebarContent, - SidebarGroup, - SidebarGroupContent, SidebarHeader, - SidebarInput, SidebarRail, + SidebarMenu, + SidebarMenuItem, + SidebarMenuButton, } from "@/components/ui/sidebar"; -import { useAppDispatch, useAppSelector } from "@/hooks/store"; -import { selectHeaders, selectRootResources, setHeaders, selectMockServerEnabled, setMockServerEnabled } from "@/state/store"; -import { Label } from "../../components/ui/label"; -import { Checkbox } from "../../components/ui/checkbox"; +import { useAppSelector } from "@/hooks/store"; +import { selectRootResources } from "@/state/store"; import { ResourceTypeList } from "@/components/resource_types/resource_type_list"; +import { Home } from "lucide-react"; +import { Link } from "react-router-dom"; // The AppSidebar. This fetches the list of root resources from the schema and displays them. export function AppSidebar({ ...props }: React.ComponentProps) { @@ -21,8 +21,16 @@ export function AppSidebar({ ...props }: React.ComponentProps) { return ( - - + + + + + + Home + + + + @@ -31,52 +39,3 @@ export function AppSidebar({ ...props }: React.ComponentProps) { ); } - -export function HeadersInput() { - const headers = useAppSelector(selectHeaders); - const dispatch = useAppDispatch(); - - const handleTextChange = (event: React.ChangeEvent) => { - dispatch(setHeaders(event.target.value)); - }; - - return ( -
- - - - - - -
- ); -} - -export function MockServerToggle() { - const mockServerEnabled = useAppSelector(selectMockServerEnabled); - const dispatch = useAppDispatch(); - - const handleToggle = (event: React.ChangeEvent) => { - dispatch(setMockServerEnabled(event.target.checked)); - }; - - return ( - - - - - - ); -} diff --git a/src/app/spec_specifier/page.tsx b/src/app/spec_specifier/page.tsx index 0c7e09c..22c35d5 100644 --- a/src/app/spec_specifier/page.tsx +++ b/src/app/spec_specifier/page.tsx @@ -1,16 +1,11 @@ -import { OASSelector } from "@/components/oas-selector"; +import { Settings } from "@/components/settings"; import { useAppSelector } from "@/hooks/store"; import { schemaState } from "@/state/store"; export default function SpecSpecifierPage() { - const state = useAppSelector(schemaState); - if(state == 'unset') { - return ( -
- -
- ) - } else { - return
- } + return ( +
+ +
+ ) } diff --git a/src/components/oas-selector.tsx b/src/components/oas-selector.tsx deleted file mode 100644 index 26224d0..0000000 --- a/src/components/oas-selector.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { useAppDispatch } from "@/hooks/store"; -import { toast } from "@/hooks/use-toast"; -import { OpenAPI } from "@/state/openapi"; -import { setSchema } from "@/state/store"; -import { useState } from "react"; -import { fetchOpenAPI, APIClient } from "@aep_dev/aep-lib-ts"; - -interface SpecSpecifier { - url: string; - prefix?: string; -} - -// A form to select an OpenAPI document URL and set it in the application state. -export function OASSelector() { - const [state, setState] = useState({ url: "", prefix: "" }); - const dispatch = useAppDispatch(); - - const onSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - try { - const openApiSpec = await fetchOpenAPI(state.url); - const apiClient = await APIClient.fromOpenAPI( - openApiSpec, - state.prefix || undefined - ); - dispatch(setSchema(new OpenAPI(apiClient))); - } catch (error) { - toast({description: `Failed to fetch OpenAPI document: ${error}`}) - } - }; - - return ( - - - AEP Explorer - -

- A web interface for exploring and interacting with{" "} - - AEP-compliant - {" "} - APIs. -

-

- What you can do: -

-
    -
  • View all resources exposed by your API
  • -
  • Browse and filter objects within collections
  • -
  • Create, read, update, and delete resources
  • -
  • Explore resource relationships and custom methods
  • -
-

- Enter a URL for your AEP-compliant, OpenAPI document to get started. -

-
-
- -
-
- - setState({ ...state, url: event.target.value })} - required - /> -

- The URL should point to a valid OpenAPI 3.0+ document for an AEP-compliant API. -

-
-
- - setState({ ...state, prefix: event.target.value })} - /> -

- If your API uses a path prefix (e.g., /api/v1), specify it here. -

-
- -
-
-
- ); -} diff --git a/src/components/settings.tsx b/src/components/settings.tsx new file mode 100644 index 0000000..07e8143 --- /dev/null +++ b/src/components/settings.tsx @@ -0,0 +1,208 @@ +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { ChevronDown } from "lucide-react"; +import { useAppDispatch, useAppSelector } from "@/hooks/store"; +import { toast } from "@/hooks/use-toast"; +import { OpenAPI } from "@/state/openapi"; +import { + setSchema, + setSpecConfig, + selectSpecConfig, + schemaState, + selectHeaders, + setHeaders, + selectMockServerEnabled, + setMockServerEnabled +} from "@/state/store"; +import { useState, useEffect, useRef } from "react"; +import { fetchOpenAPI, APIClient } from "@aep_dev/aep-lib-ts"; + +interface SpecSpecifier { + url: string; + prefix?: string; +} + +// Settings form for configuring the OpenAPI document URL, headers, and mock server. +export function Settings() { + const specConfig = useAppSelector(selectSpecConfig); + const currentSchemaState = useAppSelector(schemaState); + const headers = useAppSelector(selectHeaders); + const mockServerEnabled = useAppSelector(selectMockServerEnabled); + const [state, setState] = useState({ url: "", prefix: "" }); + const [advancedOpen, setAdvancedOpen] = useState(false); + const dispatch = useAppDispatch(); + const hasAutoLoaded = useRef(false); + + // Load persisted spec config on mount + useEffect(() => { + if (specConfig.url) { + setState({ url: specConfig.url, prefix: specConfig.prefix }); + } + }, [specConfig.url, specConfig.prefix]); + + // Auto-reload schema if URL is persisted but schema is not loaded + useEffect(() => { + const autoLoadSchema = async () => { + if (specConfig.url && currentSchemaState === 'unset' && !hasAutoLoaded.current) { + hasAutoLoaded.current = true; + try { + const openApiSpec = await fetchOpenAPI(specConfig.url); + const apiClient = await APIClient.fromOpenAPI( + openApiSpec, + specConfig.prefix || undefined + ); + dispatch(setSchema(new OpenAPI(apiClient))); + } catch (error) { + toast({description: `Failed to auto-load OpenAPI document: ${error}`}) + } + } + }; + autoLoadSchema(); + }, [specConfig.url, specConfig.prefix, currentSchemaState, dispatch]); + + const onSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + try { + const openApiSpec = await fetchOpenAPI(state.url); + const apiClient = await APIClient.fromOpenAPI( + openApiSpec, + state.prefix || undefined + ); + dispatch(setSchema(new OpenAPI(apiClient))); + // Persist the spec config + dispatch(setSpecConfig({ url: state.url, prefix: state.prefix || "" })); + } catch (error) { + toast({description: `Failed to fetch OpenAPI document: ${error}`}) + } + }; + + return ( + + + AEP Explorer + +

+ A web interface for exploring and interacting with{" "} + + AEP-compliant + {" "} + APIs. +

+

+ What you can do: +

+
    +
  • View all resources exposed by your API
  • +
  • Browse and filter objects within collections
  • +
  • Create, read, update, and delete resources
  • +
  • Explore resource relationships and custom methods
  • +
+

+ Enter a URL for your AEP-compliant, OpenAPI document to get started. +

+
+
+ +
+
+ + setState({ ...state, url: event.target.value })} + required + /> +

+ The URL should point to a valid OpenAPI 3.0+ document for an AEP-compliant API. +

+
+
+ + setState({ ...state, prefix: event.target.value })} + /> +

+ If your API uses a path prefix (e.g., /api/v1), specify it here. +

+
+ + + + + + +
+ + dispatch(setHeaders(event.target.value))} + /> +

+ Add custom HTTP headers (format: key:value,key:value) +

+
+ +
+ dispatch(setMockServerEnabled(!!checked))} + /> + +
+

+ Enable to use an in-memory mock server instead of making real API calls +

+
+
+ + +
+
+
+ ); +} diff --git a/src/state/store.ts b/src/state/store.ts index c102263..5ff03bb 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -1,6 +1,8 @@ import { configureStore } from '@reduxjs/toolkit' import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { OpenAPI, ResourceSchema } from './openapi' +import { persistStore, persistReducer, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER } from 'redux-persist' +import storage from 'redux-persist/lib/storage' // Schema reducers + selectors. interface SchemaState { @@ -101,17 +103,74 @@ const mockServerReducer = mockServerSlice.reducer; export const selectMockServerEnabled = (state: RootState) => state.mockServer.enabled; +// Spec Config reducers + selectors. +interface SpecConfigState { + url: string + prefix: string +} + +const initialSpecConfigState: SpecConfigState = { + url: "", + prefix: "" +} + +const specConfigSlice = createSlice({ + name: 'specConfig', + initialState: initialSpecConfigState, + reducers: { + setSpecConfig: (state, action: PayloadAction<{ url: string; prefix: string }>) => { + state.url = action.payload.url + state.prefix = action.payload.prefix + } + } +}) + +const {setSpecConfig} = specConfigSlice.actions; +const specConfigReducer = specConfigSlice.reducer; + +export const selectSpecConfig = (state: RootState) => state.specConfig; + +// Persist configuration +const persistConfig = { + key: 'root', + storage, + whitelist: ['headers', 'mockServer', 'specConfig'], // Only persist these slices +} + +// Combine reducers +const rootReducer = { + schema: schemaReducer, + headers: headersReducer, + mockServer: mockServerReducer, + specConfig: specConfigReducer +} + +// Create persisted reducer +const persistedReducer = persistReducer(persistConfig, + (state, action) => { + return { + schema: schemaReducer(state?.schema, action), + headers: headersReducer(state?.headers, action), + mockServer: mockServerReducer(state?.mockServer, action), + specConfig: specConfigReducer(state?.specConfig, action) + } + } +) + // Store const store = configureStore({ - reducer: { - schema: schemaReducer, - headers: headersReducer, - mockServer: mockServerReducer - } + reducer: persistedReducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], + }, + }), }) +export const persistor = persistStore(store) -export {setSchema, schemaReducer, store, setHeaders, setMockServerEnabled} +export {setSchema, schemaReducer, store, setHeaders, setMockServerEnabled, setSpecConfig} // Infer the `RootState` and `AppDispatch` types from the store itself