Extract ResourceControls, refactor ResourcesScreen layout, and add scrollable accordion panels
- Extract ResourceControls group component from ResourcesScreen: title, search, list-changed indicator, and three-section accordion (URIs, Templates, Subscriptions) with per-panel ScrollArea.Autosize - Refactor ResourcesScreen to sidebar+panel Flex layout matching HistoryScreen/LoggingScreen pattern (ScreenLayout, Sidebar, SidebarCard) - Each accordion panel dynamically computes max height based on how many sections are open, dividing available space evenly - Accordion uses controlled state (value/onChange) to track open sections - Sidebar card sizes to content naturally (compact when collapsed, grows with open sections, capped by scroll areas) - Right panel wrapped in ScrollArea.Autosize for long content - Add AllSections story with 20 URIs, 7 templates, 8 subscriptions - Add more templates and subscriptions to ConnectedView ResourcesActive - Add ResourceControls stories (Default, WithSearch, Empty) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2540617271
commit
eca6cf7665
5 changed files with 627 additions and 162 deletions
|
|
@ -0,0 +1,72 @@
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
|
import { fn } from "storybook/test";
|
||||||
|
import { ResourceControls } from "./ResourceControls";
|
||||||
|
|
||||||
|
const meta: Meta<typeof ResourceControls> = {
|
||||||
|
title: "Groups/ResourceControls",
|
||||||
|
component: ResourceControls,
|
||||||
|
args: {
|
||||||
|
searchText: "",
|
||||||
|
onSearchChange: fn(),
|
||||||
|
onRefreshList: fn(),
|
||||||
|
onSelectUri: fn(),
|
||||||
|
onSelectTemplate: fn(),
|
||||||
|
onUnsubscribeResource: fn(),
|
||||||
|
listChanged: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof ResourceControls>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
name: "config.json",
|
||||||
|
uri: "file:///config.json",
|
||||||
|
annotations: { audience: "developer", priority: 0.8 },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "README.md",
|
||||||
|
uri: "file:///README.md",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "schema.sql",
|
||||||
|
uri: "file:///schema.sql",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
templates: [
|
||||||
|
{
|
||||||
|
name: "User Profile",
|
||||||
|
uriTemplate: "file:///users/{userId}/profile",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
subscriptions: [
|
||||||
|
{
|
||||||
|
name: "config.json",
|
||||||
|
uri: "file:///config.json",
|
||||||
|
lastUpdated: "2026-03-17T10:30:00Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithSearch: Story = {
|
||||||
|
args: {
|
||||||
|
...Default.args,
|
||||||
|
searchText: "config",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Empty: Story = {
|
||||||
|
args: {
|
||||||
|
resources: [],
|
||||||
|
templates: [],
|
||||||
|
subscriptions: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Accordion, ScrollArea, Stack, TextInput, Title } from "@mantine/core";
|
||||||
|
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
|
||||||
|
import { ResourceListItem } from "../ResourceListItem/ResourceListItem";
|
||||||
|
import { ResourceSubscribedItem } from "../ResourceSubscribedItem/ResourceSubscribedItem";
|
||||||
|
import type {
|
||||||
|
ResourceItem,
|
||||||
|
TemplateListItem,
|
||||||
|
SubscriptionItem,
|
||||||
|
} from "../../screens/ResourcesScreen/ResourcesScreen";
|
||||||
|
|
||||||
|
export interface ResourceControlsProps {
|
||||||
|
resources: ResourceItem[];
|
||||||
|
templates: TemplateListItem[];
|
||||||
|
subscriptions: SubscriptionItem[];
|
||||||
|
listChanged: boolean;
|
||||||
|
searchText: string;
|
||||||
|
onSearchChange: (text: string) => void;
|
||||||
|
onRefreshList: () => void;
|
||||||
|
onSelectUri: (uri: string) => void;
|
||||||
|
onSelectTemplate: (uriTemplate: string) => void;
|
||||||
|
onUnsubscribeResource: (uri: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Available height for each accordion panel's scroll area.
|
||||||
|
// Subtracts: AppShell extra padding (md*2), screen padding (xl*2, already in calc),
|
||||||
|
// card padding (2*lg=40), title+search+gaps (~84), 3 accordion controls (~144),
|
||||||
|
// and per-panel accordion padding (~20px each).
|
||||||
|
function panelMaxHeight(openCount: number): string {
|
||||||
|
const n = Math.max(openCount, 1);
|
||||||
|
const fixedChrome = 300;
|
||||||
|
const perPanelChrome = 20;
|
||||||
|
return `calc((100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2 - ${fixedChrome + perPanelChrome * n}px) / ${n})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSectionCount(label: string, count: number): string {
|
||||||
|
return `${label} (${count})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function templateDisplayName(item: TemplateListItem): string {
|
||||||
|
return item.title ?? item.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResourceControls({
|
||||||
|
resources,
|
||||||
|
templates,
|
||||||
|
subscriptions,
|
||||||
|
listChanged,
|
||||||
|
searchText,
|
||||||
|
onSearchChange,
|
||||||
|
onRefreshList,
|
||||||
|
onSelectUri,
|
||||||
|
onSelectTemplate,
|
||||||
|
onUnsubscribeResource,
|
||||||
|
}: ResourceControlsProps) {
|
||||||
|
const filteredResources = resources.filter((r) =>
|
||||||
|
r.name.toLowerCase().includes(searchText.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultOpen = [
|
||||||
|
...(filteredResources.length > 0 ? ["resources"] : []),
|
||||||
|
...(templates.length > 0 ? ["templates"] : []),
|
||||||
|
...(subscriptions.length > 0 ? ["subscriptions"] : []),
|
||||||
|
];
|
||||||
|
|
||||||
|
const [openSections, setOpenSections] = useState<string[]>(defaultOpen);
|
||||||
|
const maxHeight = panelMaxHeight(openSections.length);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Title order={4}>Resources</Title>
|
||||||
|
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
|
||||||
|
<TextInput
|
||||||
|
placeholder="Search..."
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => onSearchChange(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<Accordion multiple value={openSections} onChange={setOpenSections}>
|
||||||
|
<Accordion.Item value="resources">
|
||||||
|
<Accordion.Control disabled={filteredResources.length === 0}>
|
||||||
|
{formatSectionCount("URIs", filteredResources.length)}
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<ScrollArea.Autosize mah={maxHeight}>
|
||||||
|
<Stack gap="xs">
|
||||||
|
{filteredResources.map((resource) => (
|
||||||
|
<ResourceListItem
|
||||||
|
key={resource.uri}
|
||||||
|
name={resource.name}
|
||||||
|
uri={resource.uri}
|
||||||
|
annotations={resource.annotations}
|
||||||
|
selected={resource.selected}
|
||||||
|
onClick={() => onSelectUri(resource.uri)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</ScrollArea.Autosize>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
|
||||||
|
<Accordion.Item value="templates">
|
||||||
|
<Accordion.Control disabled={templates.length === 0}>
|
||||||
|
{formatSectionCount("Templates", templates.length)}
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<ScrollArea.Autosize mah={maxHeight}>
|
||||||
|
<Stack gap="xs">
|
||||||
|
{templates.map((template) => (
|
||||||
|
<ResourceListItem
|
||||||
|
key={template.uriTemplate}
|
||||||
|
name={templateDisplayName(template)}
|
||||||
|
uri={template.uriTemplate}
|
||||||
|
selected={template.selected}
|
||||||
|
onClick={() => onSelectTemplate(template.uriTemplate)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</ScrollArea.Autosize>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
|
||||||
|
<Accordion.Item value="subscriptions">
|
||||||
|
<Accordion.Control disabled={subscriptions.length === 0}>
|
||||||
|
{formatSectionCount("Subscriptions", subscriptions.length)}
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<ScrollArea.Autosize mah={maxHeight}>
|
||||||
|
<Stack gap="xs">
|
||||||
|
{subscriptions.map((sub) => (
|
||||||
|
<ResourceSubscribedItem
|
||||||
|
key={sub.name}
|
||||||
|
name={sub.name}
|
||||||
|
lastUpdated={sub.lastUpdated}
|
||||||
|
onUnsubscribe={() => onUnsubscribeResource(sub.uri)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</ScrollArea.Autosize>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -48,6 +48,101 @@ const sampleResources: ResourceItem[] = [
|
||||||
annotations: { priority: 0.5 },
|
annotations: { priority: 0.5 },
|
||||||
selected: false,
|
selected: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "package.json",
|
||||||
|
uri: "file:///package.json",
|
||||||
|
annotations: { audience: "developer" },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tsconfig.json",
|
||||||
|
uri: "file:///tsconfig.json",
|
||||||
|
annotations: { audience: "developer", priority: 0.3 },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: ".env.example",
|
||||||
|
uri: "file:///.env.example",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "docker-compose.yml",
|
||||||
|
uri: "file:///docker-compose.yml",
|
||||||
|
annotations: { priority: 0.6 },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "migrations/001_init.sql",
|
||||||
|
uri: "file:///migrations/001_init.sql",
|
||||||
|
annotations: { audience: "developer", priority: 0.4 },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "migrations/002_add_users.sql",
|
||||||
|
uri: "file:///migrations/002_add_users.sql",
|
||||||
|
annotations: { audience: "developer", priority: 0.4 },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "seeds/users.json",
|
||||||
|
uri: "file:///seeds/users.json",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "seeds/products.json",
|
||||||
|
uri: "file:///seeds/products.json",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "certs/server.pem",
|
||||||
|
uri: "file:///certs/server.pem",
|
||||||
|
annotations: { priority: 0.9 },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "logs/access.log",
|
||||||
|
uri: "file:///logs/access.log",
|
||||||
|
annotations: { audience: "application", priority: 0.2 },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "logs/error.log",
|
||||||
|
uri: "file:///logs/error.log",
|
||||||
|
annotations: { audience: "application", priority: 0.7 },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "api-spec.yaml",
|
||||||
|
uri: "file:///api-spec.yaml",
|
||||||
|
annotations: { audience: "developer" },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CHANGELOG.md",
|
||||||
|
uri: "file:///CHANGELOG.md",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "LICENSE",
|
||||||
|
uri: "file:///LICENSE",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: ".gitignore",
|
||||||
|
uri: "file:///.gitignore",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Makefile",
|
||||||
|
uri: "file:///Makefile",
|
||||||
|
annotations: { audience: "developer", priority: 0.3 },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fixtures/test-data.json",
|
||||||
|
uri: "file:///fixtures/test-data.json",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const sampleTemplates: TemplateListItem[] = [
|
const sampleTemplates: TemplateListItem[] = [
|
||||||
|
|
@ -62,6 +157,75 @@ const sampleTemplates: TemplateListItem[] = [
|
||||||
uriTemplate: "db://tables/{tableName}/rows/{rowId}",
|
uriTemplate: "db://tables/{tableName}/rows/{rowId}",
|
||||||
selected: false,
|
selected: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Log File",
|
||||||
|
title: "Application Log",
|
||||||
|
uriTemplate: "file:///logs/{service}/{date}.log",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Migration",
|
||||||
|
uriTemplate: "file:///migrations/{version}_{name}.sql",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Config by Environment",
|
||||||
|
title: "Environment Config",
|
||||||
|
uriTemplate: "file:///config/{environment}.json",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "API Endpoint",
|
||||||
|
uriTemplate: "https://api.example.com/{version}/{resource}",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Report",
|
||||||
|
title: "Generated Report",
|
||||||
|
uriTemplate: "reports://{reportType}/{year}/{month}",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const sampleSubscriptions = [
|
||||||
|
{
|
||||||
|
name: "config.json",
|
||||||
|
uri: "file:///config.json",
|
||||||
|
lastUpdated: "2026-03-17T10:30:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "schema.sql",
|
||||||
|
uri: "file:///schema.sql",
|
||||||
|
lastUpdated: "2026-03-17T10:28:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "docker-compose.yml",
|
||||||
|
uri: "file:///docker-compose.yml",
|
||||||
|
lastUpdated: "2026-03-17T09:45:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "logs/error.log",
|
||||||
|
uri: "file:///logs/error.log",
|
||||||
|
lastUpdated: "2026-03-17T10:31:12Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "certs/server.pem",
|
||||||
|
uri: "file:///certs/server.pem",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "api-spec.yaml",
|
||||||
|
uri: "file:///api-spec.yaml",
|
||||||
|
lastUpdated: "2026-03-17T08:15:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "package.json",
|
||||||
|
uri: "file:///package.json",
|
||||||
|
lastUpdated: "2026-03-17T10:22:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "seeds/users.json",
|
||||||
|
uri: "file:///seeds/users.json",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const selectedResourceData: SelectedResource = {
|
const selectedResourceData: SelectedResource = {
|
||||||
|
|
@ -99,13 +263,7 @@ export const ResourceSelected: Story = {
|
||||||
r.uri === "file:///config.json" ? { ...r, selected: true } : r,
|
r.uri === "file:///config.json" ? { ...r, selected: true } : r,
|
||||||
),
|
),
|
||||||
selectedResource: selectedResourceData,
|
selectedResource: selectedResourceData,
|
||||||
subscriptions: [
|
subscriptions: sampleSubscriptions,
|
||||||
{
|
|
||||||
name: "config.json",
|
|
||||||
uri: "file:///config.json",
|
|
||||||
lastUpdated: "2026-03-17T10:30:00Z",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -152,6 +310,31 @@ export const TemplateWithResource: Story = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AllSections: Story = {
|
||||||
|
args: {
|
||||||
|
resources: sampleResources,
|
||||||
|
templates: sampleTemplates.map((t) =>
|
||||||
|
t.uriTemplate === "file:///users/{userId}/profile"
|
||||||
|
? { ...t, selected: true }
|
||||||
|
: t,
|
||||||
|
),
|
||||||
|
subscriptions: sampleSubscriptions,
|
||||||
|
selectedTemplate: selectedTemplateData,
|
||||||
|
selectedResource: {
|
||||||
|
uri: "file:///users/42/profile",
|
||||||
|
mimeType: "application/json",
|
||||||
|
annotations: { audience: "developer", priority: 0.8 },
|
||||||
|
content: JSON.stringify(
|
||||||
|
{ id: 42, name: "Alice", email: "alice@example.com" },
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
lastUpdated: "2026-03-17T11:15:00Z",
|
||||||
|
isSubscribed: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const Empty: Story = {
|
export const Empty: Story = {
|
||||||
args: {
|
args: {
|
||||||
resources: [],
|
resources: [],
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,6 @@
|
||||||
import {
|
import { Card, Flex, ScrollArea, Stack, Text } from "@mantine/core";
|
||||||
Accordion,
|
import { ResourceControls } from "../../groups/ResourceControls/ResourceControls";
|
||||||
Card,
|
|
||||||
Container,
|
|
||||||
Grid,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
Title,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
|
|
||||||
import { ResourceListItem } from "../../groups/ResourceListItem/ResourceListItem";
|
|
||||||
import { ResourcePreviewPanel } from "../../groups/ResourcePreviewPanel/ResourcePreviewPanel";
|
import { ResourcePreviewPanel } from "../../groups/ResourcePreviewPanel/ResourcePreviewPanel";
|
||||||
import { ResourceSubscribedItem } from "../../groups/ResourceSubscribedItem/ResourceSubscribedItem";
|
|
||||||
import { ResourceTemplatePanel } from "../../groups/ResourceTemplatePanel/ResourceTemplatePanel";
|
import { ResourceTemplatePanel } from "../../groups/ResourceTemplatePanel/ResourceTemplatePanel";
|
||||||
|
|
||||||
export interface ResourceItem {
|
export interface ResourceItem {
|
||||||
|
|
@ -68,15 +57,21 @@ export interface ResourcesScreenProps {
|
||||||
onUnsubscribeResource: (uri: string) => void;
|
onUnsubscribeResource: (uri: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PageContainer = Container.withProps({
|
const ScreenLayout = Flex.withProps({
|
||||||
size: "xl",
|
variant: "screen",
|
||||||
py: "xl",
|
h: "calc(100vh - var(--app-shell-header-height, 0px))",
|
||||||
|
gap: "xl",
|
||||||
|
p: "xl",
|
||||||
});
|
});
|
||||||
|
|
||||||
const FullHeightCard = Card.withProps({
|
const Sidebar = Stack.withProps({
|
||||||
|
w: 340,
|
||||||
|
flex: "0 0 auto",
|
||||||
|
});
|
||||||
|
|
||||||
|
const SidebarCard = Card.withProps({
|
||||||
withBorder: true,
|
withBorder: true,
|
||||||
padding: "lg",
|
padding: "lg",
|
||||||
h: "100%",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const DetailCard = Card.withProps({
|
const DetailCard = Card.withProps({
|
||||||
|
|
@ -90,14 +85,6 @@ const EmptyState = Text.withProps({
|
||||||
py: "xl",
|
py: "xl",
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatSectionCount(label: string, count: number): string {
|
|
||||||
return `${label} (${count})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function templateDisplayName(item: TemplateListItem): string {
|
|
||||||
return item.title ?? item.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ResourcesScreen({
|
export function ResourcesScreen({
|
||||||
resources,
|
resources,
|
||||||
templates,
|
templates,
|
||||||
|
|
@ -114,138 +101,71 @@ export function ResourcesScreen({
|
||||||
onSubscribeResource,
|
onSubscribeResource,
|
||||||
onUnsubscribeResource,
|
onUnsubscribeResource,
|
||||||
}: ResourcesScreenProps) {
|
}: ResourcesScreenProps) {
|
||||||
const filteredResources = resources.filter((r) =>
|
|
||||||
r.name.toLowerCase().includes(searchText.toLowerCase()),
|
|
||||||
);
|
|
||||||
|
|
||||||
const openSections = [
|
|
||||||
...(filteredResources.length > 0 ? ["resources"] : []),
|
|
||||||
...(templates.length > 0 ? ["templates"] : []),
|
|
||||||
...(subscriptions.length > 0 ? ["subscriptions"] : []),
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<ScreenLayout>
|
||||||
<Grid align="stretch">
|
<Sidebar>
|
||||||
<Grid.Col span={4}>
|
<SidebarCard>
|
||||||
<FullHeightCard>
|
<ResourceControls
|
||||||
<Stack gap="sm">
|
resources={resources}
|
||||||
<Title order={4}>Resources</Title>
|
templates={templates}
|
||||||
<ListChangedIndicator
|
subscriptions={subscriptions}
|
||||||
visible={listChanged}
|
listChanged={listChanged}
|
||||||
onRefresh={onRefreshList}
|
searchText={searchText}
|
||||||
/>
|
onSearchChange={onSearchChange}
|
||||||
<TextInput
|
onRefreshList={onRefreshList}
|
||||||
placeholder="Search..."
|
onSelectUri={onSelectUri}
|
||||||
value={searchText}
|
onSelectTemplate={onSelectTemplate}
|
||||||
onChange={(e) => onSearchChange(e.currentTarget.value)}
|
onUnsubscribeResource={onUnsubscribeResource}
|
||||||
/>
|
/>
|
||||||
<Accordion multiple defaultValue={openSections}>
|
</SidebarCard>
|
||||||
<Accordion.Item value="resources">
|
</Sidebar>
|
||||||
<Accordion.Control disabled={filteredResources.length === 0}>
|
|
||||||
{formatSectionCount("URIs", filteredResources.length)}
|
|
||||||
</Accordion.Control>
|
|
||||||
<Accordion.Panel>
|
|
||||||
<Stack gap="xs">
|
|
||||||
{filteredResources.map((resource) => (
|
|
||||||
<ResourceListItem
|
|
||||||
key={resource.uri}
|
|
||||||
name={resource.name}
|
|
||||||
uri={resource.uri}
|
|
||||||
annotations={resource.annotations}
|
|
||||||
selected={resource.selected}
|
|
||||||
onClick={() => onSelectUri(resource.uri)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Accordion.Panel>
|
|
||||||
</Accordion.Item>
|
|
||||||
|
|
||||||
<Accordion.Item value="templates">
|
<ScrollArea.Autosize
|
||||||
<Accordion.Control disabled={templates.length === 0}>
|
flex={1}
|
||||||
{formatSectionCount("Templates", templates.length)}
|
mah="calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2)"
|
||||||
</Accordion.Control>
|
>
|
||||||
<Accordion.Panel>
|
<Stack gap="md">
|
||||||
<Stack gap="xs">
|
{selectedTemplate ? (
|
||||||
{templates.map((template) => (
|
<>
|
||||||
<ResourceListItem
|
|
||||||
key={template.uriTemplate}
|
|
||||||
name={templateDisplayName(template)}
|
|
||||||
uri={template.uriTemplate}
|
|
||||||
selected={template.selected}
|
|
||||||
onClick={() => onSelectTemplate(template.uriTemplate)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Accordion.Panel>
|
|
||||||
</Accordion.Item>
|
|
||||||
|
|
||||||
<Accordion.Item value="subscriptions">
|
|
||||||
<Accordion.Control disabled={subscriptions.length === 0}>
|
|
||||||
{formatSectionCount("Subscriptions", subscriptions.length)}
|
|
||||||
</Accordion.Control>
|
|
||||||
<Accordion.Panel>
|
|
||||||
<Stack gap="xs">
|
|
||||||
{subscriptions.map((sub) => (
|
|
||||||
<ResourceSubscribedItem
|
|
||||||
key={sub.name}
|
|
||||||
name={sub.name}
|
|
||||||
lastUpdated={sub.lastUpdated}
|
|
||||||
onUnsubscribe={() => onUnsubscribeResource(sub.uri)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Accordion.Panel>
|
|
||||||
</Accordion.Item>
|
|
||||||
</Accordion>
|
|
||||||
</Stack>
|
|
||||||
</FullHeightCard>
|
|
||||||
</Grid.Col>
|
|
||||||
|
|
||||||
<Grid.Col span={8}>
|
|
||||||
<Stack gap="md">
|
|
||||||
{selectedTemplate ? (
|
|
||||||
<>
|
|
||||||
<DetailCard>
|
|
||||||
<ResourceTemplatePanel
|
|
||||||
{...selectedTemplate}
|
|
||||||
onReadResource={onReadResource}
|
|
||||||
/>
|
|
||||||
</DetailCard>
|
|
||||||
{selectedResource && (
|
|
||||||
<DetailCard>
|
|
||||||
<ResourcePreviewPanel
|
|
||||||
{...selectedResource}
|
|
||||||
onRefresh={() => onReadResource(selectedResource.uri)}
|
|
||||||
onSubscribe={() =>
|
|
||||||
onSubscribeResource(selectedResource.uri)
|
|
||||||
}
|
|
||||||
onUnsubscribe={() =>
|
|
||||||
onUnsubscribeResource(selectedResource.uri)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</DetailCard>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : selectedResource ? (
|
|
||||||
<DetailCard>
|
<DetailCard>
|
||||||
<ResourcePreviewPanel
|
<ResourceTemplatePanel
|
||||||
{...selectedResource}
|
{...selectedTemplate}
|
||||||
onRefresh={() => onReadResource(selectedResource.uri)}
|
onReadResource={onReadResource}
|
||||||
onSubscribe={() => onSubscribeResource(selectedResource.uri)}
|
|
||||||
onUnsubscribe={() =>
|
|
||||||
onUnsubscribeResource(selectedResource.uri)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</DetailCard>
|
</DetailCard>
|
||||||
) : (
|
{selectedResource && (
|
||||||
<DetailCard>
|
<DetailCard>
|
||||||
<EmptyState>Select a resource to preview</EmptyState>
|
<ResourcePreviewPanel
|
||||||
</DetailCard>
|
{...selectedResource}
|
||||||
)}
|
onRefresh={() => onReadResource(selectedResource.uri)}
|
||||||
</Stack>
|
onSubscribe={() =>
|
||||||
</Grid.Col>
|
onSubscribeResource(selectedResource.uri)
|
||||||
</Grid>
|
}
|
||||||
</PageContainer>
|
onUnsubscribe={() =>
|
||||||
|
onUnsubscribeResource(selectedResource.uri)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</DetailCard>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : selectedResource ? (
|
||||||
|
<DetailCard>
|
||||||
|
<ResourcePreviewPanel
|
||||||
|
{...selectedResource}
|
||||||
|
onRefresh={() => onReadResource(selectedResource.uri)}
|
||||||
|
onSubscribe={() => onSubscribeResource(selectedResource.uri)}
|
||||||
|
onUnsubscribe={() =>
|
||||||
|
onUnsubscribeResource(selectedResource.uri)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</DetailCard>
|
||||||
|
) : (
|
||||||
|
<DetailCard>
|
||||||
|
<EmptyState>Select a resource to preview</EmptyState>
|
||||||
|
</DetailCard>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</ScrollArea.Autosize>
|
||||||
|
</ScreenLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,91 @@ export const ResourcesActive: Story = {
|
||||||
annotations: { priority: 0.5 },
|
annotations: { priority: 0.5 },
|
||||||
selected: false,
|
selected: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "package.json",
|
||||||
|
uri: "file:///package.json",
|
||||||
|
annotations: { audience: "developer" },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tsconfig.json",
|
||||||
|
uri: "file:///tsconfig.json",
|
||||||
|
annotations: { audience: "developer", priority: 0.3 },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: ".env.example",
|
||||||
|
uri: "file:///.env.example",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "docker-compose.yml",
|
||||||
|
uri: "file:///docker-compose.yml",
|
||||||
|
annotations: { priority: 0.6 },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "migrations/001_init.sql",
|
||||||
|
uri: "file:///migrations/001_init.sql",
|
||||||
|
annotations: { audience: "developer", priority: 0.4 },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "migrations/002_add_users.sql",
|
||||||
|
uri: "file:///migrations/002_add_users.sql",
|
||||||
|
annotations: { audience: "developer", priority: 0.4 },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "seeds/users.json",
|
||||||
|
uri: "file:///seeds/users.json",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "seeds/products.json",
|
||||||
|
uri: "file:///seeds/products.json",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "certs/server.pem",
|
||||||
|
uri: "file:///certs/server.pem",
|
||||||
|
annotations: { priority: 0.9 },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "logs/access.log",
|
||||||
|
uri: "file:///logs/access.log",
|
||||||
|
annotations: { audience: "application", priority: 0.2 },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "logs/error.log",
|
||||||
|
uri: "file:///logs/error.log",
|
||||||
|
annotations: { audience: "application", priority: 0.7 },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "api-spec.yaml",
|
||||||
|
uri: "file:///api-spec.yaml",
|
||||||
|
annotations: { audience: "developer" },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CHANGELOG.md",
|
||||||
|
uri: "file:///CHANGELOG.md",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "LICENSE",
|
||||||
|
uri: "file:///LICENSE",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Makefile",
|
||||||
|
uri: "file:///Makefile",
|
||||||
|
annotations: { audience: "developer", priority: 0.3 },
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
templates={[
|
templates={[
|
||||||
{
|
{
|
||||||
|
|
@ -275,6 +360,34 @@ export const ResourcesActive: Story = {
|
||||||
uriTemplate: "db://tables/{tableName}/rows/{rowId}",
|
uriTemplate: "db://tables/{tableName}/rows/{rowId}",
|
||||||
selected: false,
|
selected: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Log File",
|
||||||
|
title: "Application Log",
|
||||||
|
uriTemplate: "file:///logs/{service}/{date}.log",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Migration",
|
||||||
|
uriTemplate: "file:///migrations/{version}_{name}.sql",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Config by Environment",
|
||||||
|
title: "Environment Config",
|
||||||
|
uriTemplate: "file:///config/{environment}.json",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "API Endpoint",
|
||||||
|
uriTemplate: "https://api.example.com/{version}/{resource}",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Report",
|
||||||
|
title: "Generated Report",
|
||||||
|
uriTemplate: "reports://{reportType}/{year}/{month}",
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
subscriptions={[
|
subscriptions={[
|
||||||
{
|
{
|
||||||
|
|
@ -282,6 +395,39 @@ export const ResourcesActive: Story = {
|
||||||
uri: "file:///config.json",
|
uri: "file:///config.json",
|
||||||
lastUpdated: "2026-03-17T10:30:00Z",
|
lastUpdated: "2026-03-17T10:30:00Z",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "schema.sql",
|
||||||
|
uri: "file:///schema.sql",
|
||||||
|
lastUpdated: "2026-03-17T10:28:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "docker-compose.yml",
|
||||||
|
uri: "file:///docker-compose.yml",
|
||||||
|
lastUpdated: "2026-03-17T09:45:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "logs/error.log",
|
||||||
|
uri: "file:///logs/error.log",
|
||||||
|
lastUpdated: "2026-03-17T10:31:12Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "certs/server.pem",
|
||||||
|
uri: "file:///certs/server.pem",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "api-spec.yaml",
|
||||||
|
uri: "file:///api-spec.yaml",
|
||||||
|
lastUpdated: "2026-03-17T08:15:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "package.json",
|
||||||
|
uri: "file:///package.json",
|
||||||
|
lastUpdated: "2026-03-17T10:22:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "seeds/users.json",
|
||||||
|
uri: "file:///seeds/users.json",
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
selectedTemplate={{
|
selectedTemplate={{
|
||||||
name: "User Profile",
|
name: "User Profile",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue