diff --git a/README.md b/README.md index e316973..96c3d5a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # aep-explorer +AEP Explorer is a tool designed to create a resource-oriented UI based off an OpenAPI schema. + +The tool can be run online or deployed locally depending on CORS settings. + # Running ``` npm install diff --git a/src/App.tsx b/src/App.tsx index 253aaf0..4999bf1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,10 @@ import "./App.css"; import { useState } from "react"; -import { StateContext } from "./state/StateContext"; +import { HeadersContext, StateContext } from "./state/StateContext"; import { OpenAPI, ResourceSchema } from "./state/openapi"; import SpecSpecifierPage from "./app/spec_specifier/page"; import { createBrowserRouter, RouteObject, RouterProvider } from "react-router-dom"; -import Page from "./app/explorer/page"; +import Layout from "./app/explorer/page"; import ResourceList from "./app/explorer/resource_list"; import CreateForm from "./app/explorer/form"; import InfoPage from "./app/explorer/info"; @@ -13,7 +13,7 @@ import UpdateForm from "./app/explorer/update_form"; function createRoutes(resources: ResourceSchema[]): RouteObject[] { let routes = [{ path: "/", - element: , + element: , children: [ { path: "/", @@ -21,7 +21,7 @@ function createRoutes(resources: ResourceSchema[]): RouteObject[] { }, { path: "/_explorer", - element: , + element:
, }, ].concat(resources.map((resource) => { return [ @@ -44,16 +44,17 @@ function createRoutes(resources: ResourceSchema[]): RouteObject[] { ] }).flat(1)) }]; - console.log("my routes"); - console.log(routes); return routes; } function App() { const [state, setState] = useState(new OpenAPI({})); + const [headers, setHeaders] = useState(""); return ( - - + + + + ); } diff --git a/src/app/explorer/form.tsx b/src/app/explorer/form.tsx index 4153aa0..ffecafc 100644 --- a/src/app/explorer/form.tsx +++ b/src/app/explorer/form.tsx @@ -5,6 +5,7 @@ import { useMemo, } from "react"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; import { toast } from "@/hooks/use-toast"; +import { useHeaders } from "@/state/StateContext"; type CreateFormProps = { resource: ResourceSchema @@ -14,10 +15,12 @@ export default function CreateForm(props: CreateFormProps) { const form = useForm(); const navigate = useNavigate(); + const {headers, setHeaders} = useHeaders(); + const onSubmit = ((value) => { // Value is the properly formed JSON body. // Just need to submit it and navigate back to the list page. - props.resource.create(value).then(() => { + props.resource.create(value, headers).then(() => { toast({description: `Created ${value.id}`}); navigate(-1); }) diff --git a/src/app/explorer/page.tsx b/src/app/explorer/page.tsx index 487a444..315c5d6 100644 --- a/src/app/explorer/page.tsx +++ b/src/app/explorer/page.tsx @@ -1,16 +1,23 @@ import { AppSidebar } from "@/components/app-sidebar"; +import AppBreadcrumb from "@/components/breadcrumb"; import {} from "@/components/ui/breadcrumb"; -import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; +import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { Toaster } from "@/components/ui/toaster"; import { useSpec } from "@/state/StateContext"; +import { Separator } from "@radix-ui/react-separator"; import { Outlet } from "react-router-dom"; -export default function Page() { +export default function Layout() { const { spec, setSpec } = useSpec(); return ( +
+ + + +
diff --git a/src/app/explorer/resource_list.tsx b/src/app/explorer/resource_list.tsx index 375fc3f..ebb569c 100644 --- a/src/app/explorer/resource_list.tsx +++ b/src/app/explorer/resource_list.tsx @@ -7,7 +7,7 @@ import { TableRow, } from "@/components/ui/table"; import { ColumnDef } from "@tanstack/react-table" -import { useSpec } from "@/state/StateContext"; +import { useHeaders, useSpec } from "@/state/StateContext"; import { useCallback, useEffect, useState } from "react"; import { Link, useNavigate, useParams } from "react-router-dom"; import { DataTable } from "@/components/ui/data-table"; @@ -83,6 +83,8 @@ export default function ResourceList(props: ResourceListProps) { resources: [], }); + const {headers, setHeaders} = useHeaders(); + function deleteResource(r: object) { const result = state?.resources.find((result: ResourceInstance) => result.id === r.id); if(result) { @@ -94,7 +96,7 @@ export default function ResourceList(props: ResourceListProps) { } const refreshList = useCallback(() => { props.resource - .list() + .list(headers) .then((resources) => { if (resources) { setState({ diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index 5d8e267..3d63ab3 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -5,12 +5,13 @@ import { SidebarGroup, SidebarGroupLabel, SidebarHeader, + SidebarInput, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarRail, } from "@/components/ui/sidebar"; -import { useSpec } from "@/state/StateContext"; +import { useHeaders, useSpec } from "@/state/StateContext"; import { Link } from "react-router-dom"; export function AppSidebar({ ...props }: React.ComponentProps) { @@ -34,8 +35,23 @@ export function AppSidebar({ ...props }: React.ComponentProps) { ))} +

Headers

+

key:value, comma-deliniated

+ ); } + +export function TextBoxComponent() { + const {headers, setHeaders} = useHeaders(); + + const handleTextChange = (event) => { + setHeaders(event.target.value); + }; + + return ( + + ); +} diff --git a/src/components/breadcrumb.tsx b/src/components/breadcrumb.tsx new file mode 100644 index 0000000..1238b66 --- /dev/null +++ b/src/components/breadcrumb.tsx @@ -0,0 +1,27 @@ +import { useMatches } from "react-router-dom"; +import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "./ui/breadcrumb" +import { createRouteObjects } from "@/lib/utils"; +import { useSpec } from "@/state/StateContext"; + +export default function AppBreadcrumb() { + const breadcrumbs = useMatches(); + const {spec, setSpec} = useSpec(); + console.log(breadcrumbs); + console.log(createRouteObjects(spec?.resources())); + + let breadcrumb_elements = []; + for(const i of breadcrumbs) { + breadcrumb_elements.push( + {i.pathname} + ); + breadcrumb_elements.push(); + } + + return ( + + + {breadcrumb_elements} + + + ); +} \ No newline at end of file diff --git a/src/components/nav-main.tsx b/src/components/nav-main.tsx deleted file mode 100644 index 6f68f08..0000000 --- a/src/components/nav-main.tsx +++ /dev/null @@ -1,74 +0,0 @@ -"use client"; - -import { type LucideIcon } from "lucide-react"; - -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { - SidebarGroup, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, -} from "@/components/ui/sidebar"; -import { ChevronRightIcon } from "@radix-ui/react-icons"; - -export function NavMain({ - items, -}: { - items: { - title: string; - url: string; - icon?: LucideIcon; - isActive?: boolean; - items?: { - title: string; - url: string; - }[]; - }[]; -}) { - return ( - - Platform - - {items.map((item) => ( - - - - - {item.icon && } - {item.title} - - - - - - {item.items?.map((subItem) => ( - - - - {subItem.title} - - - - ))} - - - - - ))} - - - ); -} diff --git a/src/components/nav-projects.tsx b/src/components/nav-projects.tsx deleted file mode 100644 index 7d96682..0000000 --- a/src/components/nav-projects.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { Folder, Forward, Trash2, type LucideIcon } from "lucide-react"; - -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - SidebarGroup, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuAction, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "@/components/ui/sidebar"; -import { DotsHorizontalIcon } from "@radix-ui/react-icons"; - -export function NavProjects({ - projects, -}: { - projects: { - name: string; - url: string; - icon: LucideIcon; - }[]; -}) { - const { isMobile } = useSidebar(); - - return ( - - Projects - - {projects.map((item) => ( - - - - - {item.name} - - - - - - - More - - - - - - View Project - - - - Share Project - - - - - Delete Project - - - - - ))} - - - - More - - - - - ); -} diff --git a/src/components/nav-user.tsx b/src/components/nav-user.tsx deleted file mode 100644 index 134b509..0000000 --- a/src/components/nav-user.tsx +++ /dev/null @@ -1,104 +0,0 @@ -"use client"; - -import { BadgeCheck, Bell, LogOut, Sparkles } from "lucide-react"; - -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "@/components/ui/sidebar"; -import { CaretSortIcon, ComponentPlaceholderIcon } from "@radix-ui/react-icons"; - -export function NavUser({ - user, -}: { - user: { - name: string; - email: string; - avatar: string; - }; -}) { - const { isMobile } = useSidebar(); - - return ( - - - - - - - - CN - -
- {user.name} - {user.email} -
- -
-
- - -
- - - CN - -
- {user.name} - {user.email} -
-
-
- - - - - Upgrade to Pro - - - - - - - Account - - - - Billing - - - - Notifications - - - - - - Log out - -
-
-
-
- ); -} diff --git a/src/components/team-switcher.tsx b/src/components/team-switcher.tsx deleted file mode 100644 index 5880fb1..0000000 --- a/src/components/team-switcher.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import * as React from "react"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "@/components/ui/sidebar"; -import { CaretSortIcon, PlusIcon } from "@radix-ui/react-icons"; - -export function TeamSwitcher({ - teams, -}: { - teams: { - name: string; - logo: React.ElementType; - plan: string; - }[]; -}) { - const { isMobile } = useSidebar(); - const [activeTeam, setActiveTeam] = React.useState(teams[0]); - - return ( - - - - - -
- -
-
- - {activeTeam.name} - - {activeTeam.plan} -
- -
-
- - - Teams - - {teams.map((team, index) => ( - setActiveTeam(team)} - className="gap-2 p-2" - > -
- -
- {team.name} - ⌘{index + 1} -
- ))} - - -
- -
-
Add team
-
-
-
-
-
- ); -} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index a5ef193..1c4e823 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,20 @@ +import { ResourceSchema } from "@/state/openapi"; import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +export function createRouteObjects(resources: ResourceSchema[]): object { + if(resources === null) { + return {}; + } + return resources.reduce((acc, resource) => { + acc[resource.base_url()] = `${resource.plural_name}`; + acc[`${resource.base_url()}/_create`] = `${resource.singular_name} Create`; + acc[`${resource.base_url()}/:resourceId`] = `${resource.singular_name} Info`; + acc[`${resource.base_url()}/:resourceId/_update`] = `${resource.singular_name} Update`; + return acc; + }, {}); +} \ No newline at end of file diff --git a/src/state/StateContext.tsx b/src/state/StateContext.tsx index 9d38413..a63a3d5 100644 --- a/src/state/StateContext.tsx +++ b/src/state/StateContext.tsx @@ -12,3 +12,15 @@ export const StateContext = createContext({ }); export const useSpec = () => useContext(StateContext); + +interface HeadersContext { + headers: string | null; + setHeaders: ((headers: string) => void) | null; +} + +export const HeadersContext = createContext({ + headers: null, + setHeaders: null, +}); + +export const useHeaders = () => useContext(HeadersContext); \ No newline at end of file diff --git a/src/state/fetch.ts b/src/state/fetch.ts index 755a31c..740bbc7 100644 --- a/src/state/fetch.ts +++ b/src/state/fetch.ts @@ -14,19 +14,30 @@ class ResourceInstance { this.schema = r; } - async delete() { + async delete(headers: string = "") { const url = `${this.schema.server_url}/${this.path}` - return Delete(url); + return Delete(url, headers); } - async update(value: object): Promise { + async update(value: object, headers: string = ""): Promise { const url = `${this.schema.server_url}/${this.path}` - return Patch(url, value); + return Patch(url, value, headers); } } -async function List(url: string, r: ResourceSchema): Promise { - let response = await fetch(url); +function getHeaders(headers: string): object { + const headersMap = new Map(); + const headersArray = headers.split(','); + headersArray.forEach(header => { + const [key, value] = header.split(':'); + headersMap.set(key.trim(), value.trim()); + }); + return headersMap; + +} + +async function List(url: string, r: ResourceSchema, headersString: string = ""): Promise { + let response = await fetch(url, {headers: getHeaders(headersString)}); const results: ResourceInstance[] = []; const list_response = await response.json(); for(const result of list_response.results) { @@ -35,10 +46,11 @@ async function List(url: string, r: ResourceSchema): Promise return results; } -async function Delete(url: string) { +async function Delete(url: string, headers: string = "") { try { const response = await fetch(url, { - method: 'DELETE' + method: 'DELETE', + headers: getHeaders(headers) }); if (!response.ok) { toast({description: `Delete failed with status ${response.status}`}) @@ -49,9 +61,9 @@ async function Delete(url: string) { } } -async function Get(url: string, r: ResourceSchema): Promise { +async function Get(url: string, r: ResourceSchema, headersString: string = ""): Promise { try { - const response = await fetch(url); + const response = await fetch(url, { headers: getHeaders(headersString)}); if (!response.ok) { toast({description: `Get failed with status ${response.status}`}) } @@ -62,13 +74,11 @@ async function Get(url: string, r: ResourceSchema): Promise { } } -async function Create(url: string, contents: object) { +async function Create(url: string, contents: object, headersString: string = "") { try { const response = await fetch(url, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: getHeaders(headersString), body: JSON.stringify(contents), }); if (!response.ok) { @@ -79,13 +89,11 @@ try { } } -async function Patch(url: string, contents: object) { +async function Patch(url: string, contents: object, headersString: string = "") { try { const response = await fetch(url, { method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - }, + headers: getHeaders(headersString), body: JSON.stringify(contents), }); if (!response.ok) { diff --git a/src/state/openapi.ts b/src/state/openapi.ts index 1cdecef..140f548 100644 --- a/src/state/openapi.ts +++ b/src/state/openapi.ts @@ -18,24 +18,28 @@ class ResourceSchema { this.server_url = server_url; } - list(): Promise { + list(headers: string = ""): Promise { const url = `${this.server_url}${this.base_url()}`; - return List(url, this); + return List(url, this, headers); } - get(resourceId: string): Promise { + get(resourceId: string, headers: string = ""): Promise { const url = `${this.server_url}${this.base_url()}/${resourceId}`; - return Get(url, this); + return Get(url, this, headers); } - create(body: object): Promise { + create(body: object, headers: string = ""): Promise { const url = `${this.server_url}${this.base_url()}?id=${body.id}`; return Create(url, body); } base_url(): string { const pattern = this.schema["x-aep-resource"]["patterns"][0]; - return pattern.substring(0, pattern.lastIndexOf("/")); + const subset = pattern.substring(0, pattern.lastIndexOf("/")); + if(subset[0] != "/") { + return "/" + subset; + } + return subset; } properties(): PropertySchema[] {