Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 6 additions & 66 deletions src/app/explorer/info.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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(
<p key={key}>
<b>{key}:</b> {value}
</p>,
);
}
return results;
}
return <Spinner />;
}, [state]);

const customMethods = useMemo(() => {
return props.resource.customMethods();
}, [props.resource]);

return (
<div className="space-y-6">
{/* Resource Instance card. */}
<Card>
<CardHeader>
<CardTitle>
{(state?.properties as ResourceProperties)?.path}
</CardTitle>
</CardHeader>
<CardContent>{properties}</CardContent>
</Card>

{/* Custom Methods */}
{state && customMethods.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Custom Methods</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{customMethods.map((customMethod) => (
<CustomMethodComponent
key={customMethod.name}
resourceInstance={state}
customMethod={customMethod}
/>
))}
</CardContent>
</Card>
)}

{/* Listing Child Resources */}
{childResources.map((childResource) => (
<Card key={childResource.singular_name}>
<CardHeader>
<CardTitle>{childResource.plural_name}</CardTitle>
</CardHeader>
<CardContent>
<ResourceListPage resource={childResource} />
</CardContent>
</Card>
))}
</div>
<ResourceInfo
resource={props.resource}
resourceInstance={state}
childResources={childResources}
/>
);
}
55 changes: 17 additions & 38 deletions src/app/explorer/resource_list.tsx
Original file line number Diff line number Diff line change
@@ -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[];
Expand Down Expand Up @@ -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 (
<div>
<div className="flex items-center justify-between mb-4">
<h1>
{props.resource.plural_name
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")}
</h1>
<div className="flex gap-2">
<Button
variant="outline"
size="icon"
onClick={() =>
navigate(
props.resource.substituteUrlParameters(
props.resource.base_url(),
parentParams,
) + "/_create",
)
}
>
<Plus className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" onClick={refreshList}>
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</div>
<ResourceListTable
resource={props.resource}
resources={state.resources}
parentParams={parentParams}
onRefresh={refreshList}
/>
</div>
<ListResourceComponent
resource={props.resource}
resources={state.resources}
parentParams={parentParams}
onRefresh={refreshList}
onCreate={handleCreate}
/>
);
}
90 changes: 90 additions & 0 deletions src/components/info/info.tsx
Original file line number Diff line number Diff line change
@@ -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(
<p key={key}>
<b>{key}:</b> {value}
</p>,
);
}
return results;
}
return <Spinner />;
}, [resourceInstance]);

const customMethods = useMemo(() => {
return resource.customMethods();
}, [resource]);

return (
<div className="space-y-6">
{/* Resource Instance card. */}
<Card>
<CardHeader>
<CardTitle>
{(resourceInstance?.properties as ResourceProperties)?.path}
</CardTitle>
</CardHeader>
<CardContent>{properties}</CardContent>
</Card>

{/* Custom Methods */}
{resourceInstance && customMethods.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Custom Methods</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{customMethods.map((customMethod) => (
<CustomMethodComponent
key={customMethod.name}
resourceInstance={resourceInstance}
customMethod={customMethod}
/>
))}
</CardContent>
</Card>
)}

{/* Listing Child Resources */}
{childResources.map((childResource) => (
<Card key={childResource.singular_name}>
<CardHeader>
<CardTitle>{childResource.plural_name}</CardTitle>
</CardHeader>
<CardContent>
<ResourceListPage resource={childResource} />
</CardContent>
</Card>
))}
</div>
);
}
47 changes: 47 additions & 0 deletions src/components/list/list.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string>;
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 (
<div>
<div className="flex items-center justify-between mb-4">
<h1>
{resource.plural_name
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")}
</h1>
<div className="flex gap-2">
<Button variant="outline" size="icon" onClick={onCreate}>
<Plus className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" onClick={onRefresh}>
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</div>
<ResourceListTable
resource={resource}
resources={resources}
parentParams={parentParams}
onRefresh={onRefresh}
/>
</div>
);
}
Loading