diff --git a/src/app/explorer/info.tsx b/src/app/explorer/info.tsx
index 5acd300..75681ec 100644
--- a/src/app/explorer/info.tsx
+++ b/src/app/explorer/info.tsx
@@ -1,13 +1,10 @@
import { useEffect, useMemo, useState } from "react";
import { useParams } from "react-router-dom";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { Spinner } from "@/components/ui/spinner";
import { ResourceInstance } from "@/state/fetch";
import { ResourceSchema } from "@/state/openapi";
import { useAppSelector } from "@/hooks/store";
import { selectChildResources, selectHeaders } from "@/state/store";
-import ResourceListPage from "./resource_list";
-import { CustomMethodComponent } from "@/components/custom_method";
+import { ResourceInfo } from "@/components/info/info";
type ResourceProperties = {
path: string;
@@ -55,68 +52,11 @@ export default function InfoPage(props: InfoPageProps) {
.then((instance) => setState(instance));
}, [params, props.resource, parentParams, headers]);
- const properties = useMemo(() => {
- if (state?.properties) {
- const results = [];
- for (const [key, value] of Object.entries(
- state.properties as ResourceProperties,
- )) {
- results.push(
-
- {key}: {value}
-
,
- );
- }
- return results;
- }
- return ;
- }, [state]);
-
- const customMethods = useMemo(() => {
- return props.resource.customMethods();
- }, [props.resource]);
-
return (
-
- {/* Resource Instance card. */}
-
-
-
- {(state?.properties as ResourceProperties)?.path}
-
-
- {properties}
-
-
- {/* Custom Methods */}
- {state && customMethods.length > 0 && (
-
-
- Custom Methods
-
-
- {customMethods.map((customMethod) => (
-
- ))}
-
-
- )}
-
- {/* Listing Child Resources */}
- {childResources.map((childResource) => (
-
-
- {childResource.plural_name}
-
-
-
-
-
- ))}
-
+
);
}
diff --git a/src/app/explorer/resource_list.tsx b/src/app/explorer/resource_list.tsx
index 6e71750..793ff85 100644
--- a/src/app/explorer/resource_list.tsx
+++ b/src/app/explorer/resource_list.tsx
@@ -1,12 +1,10 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
-import { Button } from "@/components/ui/button";
-import { Plus, RefreshCw } from "lucide-react";
import { ResourceSchema } from "@/state/openapi";
import { ResourceInstance } from "@/state/fetch";
import { selectHeaders } from "@/state/store";
import { useAppSelector } from "@/hooks/store";
-import { ResourceListTable } from "@/components/resource_list";
+import { ListResourceComponent } from "@/components/list/list";
type ResourceListState = {
resources: ResourceInstance[];
@@ -49,41 +47,22 @@ export default function ResourceListPage(props: ResourceListProps) {
refreshList();
}, [props, refreshList]);
+ const handleCreate = useCallback(() => {
+ navigate(
+ props.resource.substituteUrlParameters(
+ props.resource.base_url(),
+ parentParams,
+ ) + "/_create",
+ );
+ }, [navigate, props.resource, parentParams]);
+
return (
-
-
-
- {props.resource.plural_name
- .split(" ")
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
- .join(" ")}
-
-
-
-
-
-
-
-
+
);
}
diff --git a/src/components/info/info.tsx b/src/components/info/info.tsx
new file mode 100644
index 0000000..36b9d9a
--- /dev/null
+++ b/src/components/info/info.tsx
@@ -0,0 +1,90 @@
+import { useMemo } from "react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Spinner } from "@/components/ui/spinner";
+import { ResourceInstance } from "@/state/fetch";
+import { ResourceSchema } from "@/state/openapi";
+import { CustomMethodComponent } from "@/components/custom_method";
+import ResourceListPage from "@/app/explorer/resource_list";
+
+type ResourceProperties = {
+ path: string;
+ [key: string]: string | number | boolean;
+};
+
+type ResourceInfoProps = {
+ resource: ResourceSchema;
+ resourceInstance: ResourceInstance | null;
+ childResources: ResourceSchema[];
+};
+
+// ResourceInfo renders the details of a resource instance, its custom methods,
+// and listings of its child resources. It is a presentational component and
+// does not fetch data or depend on routing state.
+export function ResourceInfo(props: ResourceInfoProps) {
+ const { resource, resourceInstance, childResources } = props;
+
+ const properties = useMemo(() => {
+ if (resourceInstance?.properties) {
+ const results = [];
+ for (const [key, value] of Object.entries(
+ resourceInstance.properties as ResourceProperties,
+ )) {
+ results.push(
+
+ {key}: {value}
+
,
+ );
+ }
+ return results;
+ }
+ return ;
+ }, [resourceInstance]);
+
+ const customMethods = useMemo(() => {
+ return resource.customMethods();
+ }, [resource]);
+
+ return (
+
+ {/* Resource Instance card. */}
+
+
+
+ {(resourceInstance?.properties as ResourceProperties)?.path}
+
+
+ {properties}
+
+
+ {/* Custom Methods */}
+ {resourceInstance && customMethods.length > 0 && (
+
+
+ Custom Methods
+
+
+ {customMethods.map((customMethod) => (
+
+ ))}
+
+
+ )}
+
+ {/* Listing Child Resources */}
+ {childResources.map((childResource) => (
+
+
+ {childResource.plural_name}
+
+
+
+
+
+ ))}
+
+ );
+}
diff --git a/src/components/list/list.tsx b/src/components/list/list.tsx
new file mode 100644
index 0000000..4f5d4ce
--- /dev/null
+++ b/src/components/list/list.tsx
@@ -0,0 +1,47 @@
+import { Button } from "@/components/ui/button";
+import { Plus, RefreshCw } from "lucide-react";
+import { ResourceSchema } from "@/state/openapi";
+import { ResourceInstance } from "@/state/fetch";
+import { ResourceListTable } from "@/components/resource_list";
+
+type ListResourceComponentProps = {
+ resource: ResourceSchema;
+ resources: ResourceInstance[];
+ parentParams: Map;
+ onRefresh: () => void;
+ onCreate: () => void;
+};
+
+// ListResourceComponent renders the header (title, create, refresh) and
+// table for a list of resource instances. It is a presentational component
+// and does not fetch data or depend on routing state.
+export function ListResourceComponent(props: ListResourceComponentProps) {
+ const { resource, resources, parentParams, onRefresh, onCreate } = props;
+
+ return (
+
+
+
+ {resource.plural_name
+ .split(" ")
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(" ")}
+
+
+
+
+
+ );
+}