mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-27 06:08:34 +00:00
Fix: added header
This commit is contained in:
parent
8658342168
commit
673809576c
137
apps/webapp/app/components/common/page-header.tsx
Normal file
137
apps/webapp/app/components/common/page-header.tsx
Normal file
@ -0,0 +1,137 @@
|
||||
import { useNavigate } from "@remix-run/react";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||
import { SidebarTrigger } from "~/components/ui/sidebar";
|
||||
|
||||
export interface BreadcrumbItem {
|
||||
label: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export interface PageHeaderAction {
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
onClick: () => void;
|
||||
variant?: "default" | "secondary" | "outline" | "ghost";
|
||||
}
|
||||
|
||||
export interface PageHeaderTab {
|
||||
label: string;
|
||||
value: string;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export interface PageHeaderProps {
|
||||
title: string;
|
||||
breadcrumbs?: BreadcrumbItem[];
|
||||
actions?: PageHeaderAction[];
|
||||
tabs?: PageHeaderTab[];
|
||||
showBackForward?: boolean;
|
||||
}
|
||||
|
||||
// Back and Forward navigation component
|
||||
function NavigationBackForward() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="mr-1 flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
aria-label="Back"
|
||||
onClick={() => navigate(-1)}
|
||||
className="rounded"
|
||||
type="button"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
aria-label="Forward"
|
||||
onClick={() => navigate(1)}
|
||||
className="rounded"
|
||||
type="button"
|
||||
>
|
||||
<ArrowRight size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PageHeader({
|
||||
title,
|
||||
breadcrumbs,
|
||||
actions,
|
||||
tabs,
|
||||
showBackForward = true,
|
||||
}: PageHeaderProps) {
|
||||
return (
|
||||
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b border-gray-300 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||
<div className="flex w-full items-center justify-between gap-1 px-4 pr-2 lg:gap-2">
|
||||
<div className="-ml-1 flex items-center gap-1">
|
||||
{/* Back/Forward navigation before SidebarTrigger */}
|
||||
{showBackForward && <NavigationBackForward />}
|
||||
<SidebarTrigger className="mr-1" />
|
||||
|
||||
{/* Breadcrumbs */}
|
||||
{breadcrumbs && breadcrumbs.length > 0 ? (
|
||||
<nav className="mt-0.5 flex items-center space-x-1">
|
||||
{breadcrumbs.map((breadcrumb, index) => (
|
||||
<div key={index} className="flex items-center">
|
||||
{index > 0 && (
|
||||
<span className="text-muted-foreground mx-1">/</span>
|
||||
)}
|
||||
{breadcrumb.href ? (
|
||||
<a href={breadcrumb.href}>{breadcrumb.label}</a>
|
||||
) : (
|
||||
<span className="text-gray-900">{breadcrumb.label}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
) : (
|
||||
<h1 className="text-base">{title}</h1>
|
||||
)}
|
||||
|
||||
{/* Tabs */}
|
||||
{tabs && tabs.length > 0 && (
|
||||
<div className="ml-2 flex items-center gap-0.5">
|
||||
{tabs.map((tab) => (
|
||||
<Button
|
||||
key={tab.value}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded"
|
||||
isActive={tab.isActive}
|
||||
onClick={tab.onClick}
|
||||
aria-current={tab.isActive ? "page" : undefined}
|
||||
>
|
||||
{tab.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
{actions && actions.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
{actions.map((action, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
onClick={action.onClick}
|
||||
variant={action.variant || "secondary"}
|
||||
className="gap-2"
|
||||
>
|
||||
{action.icon}
|
||||
{action.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@ -5,6 +5,7 @@ import { UserTypeEnum } from "@core/types";
|
||||
import { type ConversationHistory } from "@core/database";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { extensionsForConversation } from "./editor-extensions";
|
||||
import { skillExtension } from "../editor/skill-extension";
|
||||
|
||||
interface AIConversationItemProps {
|
||||
conversationHistory: ConversationHistory;
|
||||
@ -20,7 +21,7 @@ export const ConversationItem = ({
|
||||
const id = `a${conversationHistory.id.replace(/-/g, "")}`;
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [...extensionsForConversation],
|
||||
extensions: [...extensionsForConversation, skillExtension],
|
||||
editable: false,
|
||||
content: conversationHistory.message,
|
||||
});
|
||||
|
||||
14
apps/webapp/app/components/editor/conversation-context.tsx
Normal file
14
apps/webapp/app/components/editor/conversation-context.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
|
||||
interface ConversationContextInterface {
|
||||
conversationHistoryId: string;
|
||||
|
||||
// Used just in streaming
|
||||
streaming?: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
actionMessages?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const ConversationContext = React.createContext<
|
||||
ConversationContextInterface | undefined
|
||||
>(undefined);
|
||||
@ -0,0 +1 @@
|
||||
export * from './skill-extension';
|
||||
@ -0,0 +1,61 @@
|
||||
import { NodeViewWrapper } from "@tiptap/react";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { getIcon as iconUtil, type IconType } from "../../icon-utils";
|
||||
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const SkillComponent = (props: any) => {
|
||||
const id = props.node.attrs.id;
|
||||
const name = props.node.attrs.name;
|
||||
const agent = props.node.attrs.agent;
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
if (id === "undefined" || id === undefined || !name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getIcon = () => {
|
||||
const Icon = iconUtil(agent as IconType);
|
||||
|
||||
return <Icon size={18} className="rounded-sm" />;
|
||||
};
|
||||
|
||||
const snakeToTitleCase = (input: string): string => {
|
||||
if (!input) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const words = input.split("_");
|
||||
|
||||
// First word: capitalize first letter
|
||||
const firstWord =
|
||||
words[0].charAt(0).toUpperCase() + words[0].slice(1).toLowerCase();
|
||||
|
||||
// Rest of the words: all lowercase
|
||||
const restWords = words.slice(1).map((word) => word.toLowerCase());
|
||||
|
||||
// Join with spaces
|
||||
return [firstWord, ...restWords].join(" ");
|
||||
};
|
||||
|
||||
const getComponent = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
{getIcon()}
|
||||
<span className="font-mono text-sm">{snakeToTitleCase(name)}</span>
|
||||
</div>
|
||||
<div className="px-0">
|
||||
{!open ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="inline w-fit">{getComponent()}</NodeViewWrapper>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,47 @@
|
||||
import { ReactNodeViewRenderer, Node, mergeAttributes } from "@tiptap/react";
|
||||
|
||||
import { SkillComponent } from "./skill-component";
|
||||
|
||||
export const skillExtension = Node.create({
|
||||
name: "skill",
|
||||
group: "block",
|
||||
atom: true,
|
||||
selectable: false,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
id: {
|
||||
default: undefined,
|
||||
},
|
||||
name: {
|
||||
default: undefined,
|
||||
},
|
||||
agent: {
|
||||
default: undefined,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "skill",
|
||||
getAttrs: (element) => {
|
||||
return {
|
||||
id: element.getAttribute("id"),
|
||||
name: element.getAttribute("name"),
|
||||
agent: element.getAttribute("agent"),
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["skill", mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(SkillComponent);
|
||||
},
|
||||
});
|
||||
@ -4,7 +4,7 @@ import {
|
||||
} from "@remix-run/server-runtime";
|
||||
import { sort } from "fast-sort";
|
||||
|
||||
import { useParams, useRevalidator } from "@remix-run/react";
|
||||
import { useParams, useRevalidator, useNavigate } from "@remix-run/react";
|
||||
import {
|
||||
requireUser,
|
||||
requireUserId,
|
||||
@ -25,6 +25,8 @@ import {
|
||||
import { useTypedLoaderData } from "remix-typedjson";
|
||||
import React from "react";
|
||||
import { ScrollAreaWithAutoScroll } from "~/components/use-auto-scroll";
|
||||
import { PageHeader } from "~/components/common/page-header";
|
||||
import { Plus } from "lucide-react";
|
||||
|
||||
import { json } from "@remix-run/node";
|
||||
import { env } from "~/env.server";
|
||||
@ -84,6 +86,8 @@ export default function SingleConversation() {
|
||||
const { conversationId } = useParams();
|
||||
const revalidator = useRevalidator();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (run) {
|
||||
setConversationResponse(run);
|
||||
@ -129,49 +133,57 @@ export default function SingleConversation() {
|
||||
}
|
||||
|
||||
return (
|
||||
<ResizablePanelGroup direction="horizontal" className="!rounded-md">
|
||||
<ResizableHandle className="w-1" />
|
||||
<>
|
||||
<PageHeader
|
||||
title="Conversation"
|
||||
breadcrumbs={[
|
||||
{ label: "Conversations", href: "/home/conversation" },
|
||||
{ label: conversation.title || "Untitled" },
|
||||
]}
|
||||
actions={[
|
||||
{
|
||||
label: "New conversation",
|
||||
icon: <Plus size={14} />,
|
||||
onClick: () => navigate("/home/conversation"),
|
||||
variant: "secondary",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<ResizablePanel
|
||||
collapsible
|
||||
collapsedSize={0}
|
||||
className="flex w-full flex-col"
|
||||
>
|
||||
<div className="relative flex h-[calc(100vh_-_70px)] w-full flex-col items-center justify-center overflow-auto">
|
||||
<div className="flex h-[calc(100vh_-_56px)] w-full flex-col justify-end overflow-hidden">
|
||||
<ScrollAreaWithAutoScroll>
|
||||
{getConversations()}
|
||||
{conversationResponse && (
|
||||
<StreamingConversation
|
||||
runId={conversationResponse.id}
|
||||
token={conversationResponse.token}
|
||||
afterStreaming={() => {
|
||||
setConversationResponse(undefined);
|
||||
revalidator.revalidate();
|
||||
}}
|
||||
apiURL={apiURL}
|
||||
<div className="relative flex h-[calc(100vh_-_56px)] w-full flex-col items-center justify-center overflow-auto">
|
||||
<div className="flex h-[calc(100vh_-_80px)] w-full flex-col justify-end overflow-hidden">
|
||||
<ScrollAreaWithAutoScroll>
|
||||
{getConversations()}
|
||||
{conversationResponse && (
|
||||
<StreamingConversation
|
||||
runId={conversationResponse.id}
|
||||
token={conversationResponse.token}
|
||||
afterStreaming={() => {
|
||||
setConversationResponse(undefined);
|
||||
revalidator.revalidate();
|
||||
}}
|
||||
apiURL={apiURL}
|
||||
/>
|
||||
)}
|
||||
</ScrollAreaWithAutoScroll>
|
||||
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<div className="w-full max-w-[97ch] px-1 pr-2">
|
||||
{conversation?.status !== "need_approval" && (
|
||||
<ConversationTextarea
|
||||
conversationId={conversationId as string}
|
||||
className="bg-background-3 w-full border-1 border-gray-300"
|
||||
isLoading={
|
||||
!!conversationResponse ||
|
||||
conversation?.status === "running" ||
|
||||
stopLoading
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</ScrollAreaWithAutoScroll>
|
||||
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<div className="w-full max-w-[97ch] px-1 pr-2">
|
||||
{conversation?.status !== "need_approval" && (
|
||||
<ConversationTextarea
|
||||
conversationId={conversationId as string}
|
||||
className="bg-background-3 w-full border-1 border-gray-300"
|
||||
isLoading={
|
||||
!!conversationResponse ||
|
||||
conversation?.status === "running" ||
|
||||
stopLoading
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
CreateConversationSchema,
|
||||
} from "~/services/conversation.server";
|
||||
import { json } from "@remix-run/node";
|
||||
import { PageHeader } from "~/components/common/page-header";
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// Only return userId, not the heavy nodeLinks
|
||||
@ -67,6 +68,9 @@ export default function Chat() {
|
||||
const { user } = useTypedLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<>{typeof window !== "undefined" && <ConversationNew user={user} />}</>
|
||||
<>
|
||||
<PageHeader title="Conversation" />
|
||||
{typeof window !== "undefined" && <ConversationNew user={user} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import { SearchBodyRequest } from "./search";
|
||||
import { SearchService } from "~/services/search.server";
|
||||
import { GraphVisualizationClient } from "~/components/graph/graph-client";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
import { PageHeader } from "~/components/common/page-header";
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
@ -84,18 +85,21 @@ export default function Dashboard() {
|
||||
}, [userId]);
|
||||
|
||||
return (
|
||||
<div className="home flex h-[calc(100vh_-_56px)] flex-col overflow-y-auto p-3 text-base">
|
||||
<div className="flex grow items-center justify-center rounded">
|
||||
{loading ? (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||
<LoaderCircle size={18} className="mr-1 animate-spin" />
|
||||
<span className="text-muted-foreground">Loading graph...</span>
|
||||
</div>
|
||||
) : (
|
||||
typeof window !== "undefined" &&
|
||||
nodeLinks && <GraphVisualizationClient triplets={nodeLinks} />
|
||||
)}
|
||||
<>
|
||||
<PageHeader title="Memory graph" />
|
||||
<div className="home flex h-[calc(100vh_-_56px)] flex-col overflow-y-auto p-3 text-base">
|
||||
<div className="flex grow items-center justify-center rounded">
|
||||
{loading ? (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||
<LoaderCircle size={18} className="mr-1 animate-spin" />
|
||||
<span className="text-muted-foreground">Loading graph...</span>
|
||||
</div>
|
||||
) : (
|
||||
typeof window !== "undefined" &&
|
||||
nodeLinks && <GraphVisualizationClient triplets={nodeLinks} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -20,6 +20,8 @@ import {
|
||||
upsertIngestionRule,
|
||||
} from "~/services/ingestionRule.server";
|
||||
import { Section } from "~/components/integrations/section";
|
||||
import { PageHeader } from "~/components/common/page-header";
|
||||
import { Plus } from "lucide-react";
|
||||
|
||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
@ -139,84 +141,105 @@ export default function IntegrationDetail() {
|
||||
const Component = getIcon(integration.icon as IconType);
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden p-4 px-5">
|
||||
<Section
|
||||
title={integration.name}
|
||||
description={integration.description}
|
||||
icon={
|
||||
<div className="bg-grayAlpha-100 flex h-12 w-12 items-center justify-center rounded">
|
||||
<Component size={24} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
{/* Authentication Methods */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Authentication Methods</h3>
|
||||
<div className="space-y-2">
|
||||
{hasApiKey && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex items-center gap-2 text-sm">
|
||||
<Checkbox checked /> API Key authentication
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{hasOAuth2 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex items-center gap-2 text-sm">
|
||||
<Checkbox checked />
|
||||
OAuth 2.0 authentication
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{!hasApiKey && !hasOAuth2 && !hasMCPAuth && (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
No authentication method specified
|
||||
</div>
|
||||
)}
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
<PageHeader
|
||||
title="Integrations"
|
||||
breadcrumbs={[
|
||||
{ label: "Integrations", href: "/home/integrations" },
|
||||
{ label: integration?.name || "Untitled" },
|
||||
]}
|
||||
actions={[
|
||||
{
|
||||
label: "Request New Integration",
|
||||
icon: <Plus size={14} />,
|
||||
onClick: () =>
|
||||
window.open(
|
||||
"https://github.com/redplanethq/core/issues/new",
|
||||
"_blank",
|
||||
),
|
||||
variant: "secondary",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="h-[calc(100vh_-_56px)] overflow-hidden p-4 px-5">
|
||||
<Section
|
||||
title={integration.name}
|
||||
description={integration.description}
|
||||
icon={
|
||||
<div className="bg-grayAlpha-100 flex h-12 w-12 items-center justify-center rounded">
|
||||
<Component size={24} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Connect Section */}
|
||||
{!activeAccount && (hasApiKey || hasOAuth2) && (
|
||||
<div className="mt-6 space-y-4">
|
||||
<h3 className="text-lg font-medium">
|
||||
Connect to {integration.name}
|
||||
</h3>
|
||||
|
||||
{/* API Key Authentication */}
|
||||
<ApiKeyAuthSection
|
||||
integration={integration}
|
||||
specData={specData}
|
||||
activeAccount={activeAccount}
|
||||
/>
|
||||
|
||||
{/* OAuth Authentication */}
|
||||
<OAuthAuthSection
|
||||
integration={integration}
|
||||
specData={specData}
|
||||
activeAccount={activeAccount}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
{/* Authentication Methods */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Authentication Methods</h3>
|
||||
<div className="space-y-2">
|
||||
{hasApiKey && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex items-center gap-2 text-sm">
|
||||
<Checkbox checked /> API Key authentication
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{hasOAuth2 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex items-center gap-2 text-sm">
|
||||
<Checkbox checked />
|
||||
OAuth 2.0 authentication
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{!hasApiKey && !hasOAuth2 && !hasMCPAuth && (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
No authentication method specified
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Connected Account Info */}
|
||||
<ConnectedAccountSection activeAccount={activeAccount} />
|
||||
{/* Connect Section */}
|
||||
{!activeAccount && (hasApiKey || hasOAuth2) && (
|
||||
<div className="mt-6 space-y-4">
|
||||
<h3 className="text-lg font-medium">
|
||||
Connect to {integration.name}
|
||||
</h3>
|
||||
|
||||
{/* MCP Authentication Section */}
|
||||
<MCPAuthSection
|
||||
integration={integration}
|
||||
activeAccount={activeAccount as any}
|
||||
hasMCPAuth={hasMCPAuth}
|
||||
/>
|
||||
{/* API Key Authentication */}
|
||||
<ApiKeyAuthSection
|
||||
integration={integration}
|
||||
specData={specData}
|
||||
activeAccount={activeAccount}
|
||||
/>
|
||||
|
||||
{/* Ingestion Rule Section */}
|
||||
<IngestionRuleSection
|
||||
ingestionRule={ingestionRule}
|
||||
activeAccount={activeAccount}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
{/* OAuth Authentication */}
|
||||
<OAuthAuthSection
|
||||
integration={integration}
|
||||
specData={specData}
|
||||
activeAccount={activeAccount}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Connected Account Info */}
|
||||
<ConnectedAccountSection activeAccount={activeAccount} />
|
||||
|
||||
{/* MCP Authentication Section */}
|
||||
<MCPAuthSection
|
||||
integration={integration}
|
||||
activeAccount={activeAccount as any}
|
||||
hasMCPAuth={hasMCPAuth}
|
||||
/>
|
||||
|
||||
{/* Ingestion Rule Section */}
|
||||
<IngestionRuleSection
|
||||
ingestionRule={ingestionRule}
|
||||
activeAccount={activeAccount}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,6 +6,8 @@ import { requireUserId, requireWorkpace } from "~/services/session.server";
|
||||
import { getIntegrationDefinitions } from "~/services/integrationDefinition.server";
|
||||
import { getIntegrationAccounts } from "~/services/integrationAccount.server";
|
||||
import { IntegrationGrid } from "~/components/integrations/integration-grid";
|
||||
import { PageHeader } from "~/components/common/page-header";
|
||||
import { Plus } from "lucide-react";
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
@ -38,11 +40,28 @@ export default function Integrations() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="home flex h-full flex-col overflow-y-auto p-4 px-5">
|
||||
<IntegrationGrid
|
||||
integrations={integrationDefinitions}
|
||||
activeAccountIds={activeAccountIds}
|
||||
<div className="flex h-full flex-col">
|
||||
<PageHeader
|
||||
title="Integrations"
|
||||
actions={[
|
||||
{
|
||||
label: "Request New Integration",
|
||||
icon: <Plus size={14} />,
|
||||
onClick: () =>
|
||||
window.open(
|
||||
"https://github.com/redplanethq/core/issues/new",
|
||||
"_blank",
|
||||
),
|
||||
variant: "secondary",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="home flex h-[calc(100vh_-_56px)] flex-col overflow-y-auto p-4 px-5">
|
||||
<IntegrationGrid
|
||||
integrations={integrationDefinitions}
|
||||
activeAccountIds={activeAccountIds}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "@remix-run/react";
|
||||
import { useLogs } from "~/hooks/use-logs";
|
||||
import { LogsFilters } from "~/components/logs/logs-filters";
|
||||
import { VirtualLogsList } from "~/components/logs/virtual-logs-list";
|
||||
import { AppContainer, PageContainer } from "~/components/layout/app-layout";
|
||||
import { Card, CardContent } from "~/components/ui/card";
|
||||
import { Activity } from "lucide-react";
|
||||
import { PageHeader } from "~/components/common/page-header";
|
||||
|
||||
export default function LogsActivity() {
|
||||
const navigate = useNavigate();
|
||||
const [selectedSource, setSelectedSource] = useState<string | undefined>();
|
||||
const [selectedStatus, setSelectedStatus] = useState<string | undefined>();
|
||||
|
||||
@ -36,42 +39,63 @@ export default function LogsActivity() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-4 px-5">
|
||||
<LogsFilters
|
||||
availableSources={availableSources}
|
||||
selectedSource={selectedSource}
|
||||
selectedStatus={selectedStatus}
|
||||
onSourceChange={setSelectedSource}
|
||||
onStatusChange={setSelectedStatus}
|
||||
<div className="flex h-full flex-col">
|
||||
<PageHeader
|
||||
title="Logs"
|
||||
tabs={[
|
||||
{
|
||||
label: "All",
|
||||
value: "all",
|
||||
isActive: false,
|
||||
onClick: () => navigate("/home/logs/all"),
|
||||
},
|
||||
{
|
||||
label: "Activity",
|
||||
value: "activity",
|
||||
isActive: true,
|
||||
onClick: () => navigate("/home/logs/activity"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Logs List */}
|
||||
<div className="space-y-4">
|
||||
{logs.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="bg-background-2 flex items-center justify-center py-16">
|
||||
<div className="text-center">
|
||||
<Activity className="text-muted-foreground mx-auto mb-4 h-12 w-12" />
|
||||
<h3 className="mb-2 text-lg font-semibold">
|
||||
No activity logs found
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
{selectedSource || selectedStatus
|
||||
? "Try adjusting your filters to see more results."
|
||||
: "No activity ingestion logs are available yet."}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<VirtualLogsList
|
||||
logs={logs}
|
||||
hasMore={hasMore}
|
||||
loadMore={loadMore}
|
||||
isLoading={isLoading}
|
||||
height={600}
|
||||
<div className="flex h-[calc(100vh_-_56px)] flex-col space-y-6 p-4 px-5">
|
||||
{logs.length > 0 && (
|
||||
<LogsFilters
|
||||
availableSources={availableSources}
|
||||
selectedSource={selectedSource}
|
||||
selectedStatus={selectedStatus}
|
||||
onSourceChange={setSelectedSource}
|
||||
onStatusChange={setSelectedStatus}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Logs List */}
|
||||
<div className="space-y-4">
|
||||
{logs.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="bg-background-2 flex items-center justify-center py-16">
|
||||
<div className="text-center">
|
||||
<Activity className="text-muted-foreground mx-auto mb-4 h-12 w-12" />
|
||||
<h3 className="mb-2 text-lg font-semibold">
|
||||
No activity logs found
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
{selectedSource || selectedStatus
|
||||
? "Try adjusting your filters to see more results."
|
||||
: "No activity ingestion logs are available yet."}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<VirtualLogsList
|
||||
logs={logs}
|
||||
hasMore={hasMore}
|
||||
loadMore={loadMore}
|
||||
isLoading={isLoading}
|
||||
height={600}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "@remix-run/react";
|
||||
import { useLogs } from "~/hooks/use-logs";
|
||||
import { LogsFilters } from "~/components/logs/logs-filters";
|
||||
import { VirtualLogsList } from "~/components/logs/virtual-logs-list";
|
||||
import { AppContainer, PageContainer } from "~/components/layout/app-layout";
|
||||
import { Card, CardContent } from "~/components/ui/card";
|
||||
import { Database } from "lucide-react";
|
||||
import { PageHeader } from "~/components/common/page-header";
|
||||
|
||||
export default function LogsAll() {
|
||||
const navigate = useNavigate();
|
||||
const [selectedSource, setSelectedSource] = useState<string | undefined>();
|
||||
const [selectedStatus, setSelectedStatus] = useState<string | undefined>();
|
||||
|
||||
@ -36,42 +39,63 @@ export default function LogsAll() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-4 px-5">
|
||||
{/* Filters */}
|
||||
<LogsFilters
|
||||
availableSources={availableSources}
|
||||
selectedSource={selectedSource}
|
||||
selectedStatus={selectedStatus}
|
||||
onSourceChange={setSelectedSource}
|
||||
onStatusChange={setSelectedStatus}
|
||||
<>
|
||||
<PageHeader
|
||||
title="Logs"
|
||||
tabs={[
|
||||
{
|
||||
label: "All",
|
||||
value: "all",
|
||||
isActive: true,
|
||||
onClick: () => navigate("/home/logs/all"),
|
||||
},
|
||||
{
|
||||
label: "Activity",
|
||||
value: "activity",
|
||||
isActive: false,
|
||||
onClick: () => navigate("/home/logs/activity"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Logs List */}
|
||||
<div className="space-y-4">
|
||||
{logs.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="bg-background-2 flex items-center justify-center py-16">
|
||||
<div className="text-center">
|
||||
<Database className="text-muted-foreground mx-auto mb-4 h-12 w-12" />
|
||||
<h3 className="mb-2 text-lg font-semibold">No logs found</h3>
|
||||
<p className="text-muted-foreground">
|
||||
{selectedSource || selectedStatus
|
||||
? "Try adjusting your filters to see more results."
|
||||
: "No ingestion logs are available yet."}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<VirtualLogsList
|
||||
logs={logs}
|
||||
hasMore={hasMore}
|
||||
loadMore={loadMore}
|
||||
isLoading={isLoading}
|
||||
height={600}
|
||||
<div className="space-y-6 p-4 px-5">
|
||||
{/* Filters */}
|
||||
{logs.length > 0 && (
|
||||
<LogsFilters
|
||||
availableSources={availableSources}
|
||||
selectedSource={selectedSource}
|
||||
selectedStatus={selectedStatus}
|
||||
onSourceChange={setSelectedSource}
|
||||
onStatusChange={setSelectedStatus}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Logs List */}
|
||||
<div className="space-y-4">
|
||||
{logs.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="bg-background-2 flex items-center justify-center py-16">
|
||||
<div className="text-center">
|
||||
<Database className="text-muted-foreground mx-auto mb-4 h-12 w-12" />
|
||||
<h3 className="mb-2 text-lg font-semibold">No logs found</h3>
|
||||
<p className="text-muted-foreground">
|
||||
{selectedSource || selectedStatus
|
||||
? "Try adjusting your filters to see more results."
|
||||
: "No ingestion logs are available yet."}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<VirtualLogsList
|
||||
logs={logs}
|
||||
hasMore={hasMore}
|
||||
loadMore={loadMore}
|
||||
isLoading={isLoading}
|
||||
height={600}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,7 +7,6 @@ import { clearRedirectTo, commitSession } from "~/services/redirectTo.server";
|
||||
|
||||
import { AppSidebar } from "~/components/sidebar/app-sidebar";
|
||||
import { SidebarInset, SidebarProvider } from "~/components/ui/sidebar";
|
||||
import { SiteHeader } from "~/components/ui/header";
|
||||
import { FloatingIngestionStatus } from "~/components/ingestion/floating-ingestion-status";
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
@ -40,8 +39,7 @@ export default function Home() {
|
||||
>
|
||||
<AppSidebar variant="inset" />
|
||||
<SidebarInset className="bg-background-2 h-full rounded pr-0">
|
||||
<SiteHeader />
|
||||
<div className="flex h-[calc(100vh_-_56px)] flex-col rounded">
|
||||
<div className="flex h-full flex-col rounded">
|
||||
<div className="@container/main flex h-full flex-col gap-2">
|
||||
<div className="flex h-full flex-col">
|
||||
<Outlet />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user