mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-11 10:08:27 +00:00
Feat: Ingest UI
This commit is contained in:
parent
02c7b90374
commit
a9034fb448
75
apps/webapp/app/components/api/api-table.tsx
Normal file
75
apps/webapp/app/components/api/api-table.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import {
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "../ui/table";
|
||||||
|
import { type PersonalAccessToken, useTokensColumns } from "./columns";
|
||||||
|
|
||||||
|
export const APITable = ({
|
||||||
|
personalAccessTokens,
|
||||||
|
}: {
|
||||||
|
personalAccessTokens: PersonalAccessToken[];
|
||||||
|
}) => {
|
||||||
|
const columns = useTokensColumns();
|
||||||
|
const table = useReactTable({
|
||||||
|
data: personalAccessTokens,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id} className="text-sm">
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
data-state={row.getIsSelected() && "selected"}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id} className="w-[90%] py-1">
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={columns.length}
|
||||||
|
className="h-24 text-center"
|
||||||
|
></TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
116
apps/webapp/app/components/api/columns.tsx
Normal file
116
apps/webapp/app/components/api/columns.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { useFetcher } from "@remix-run/react";
|
||||||
|
import { type ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { Button } from "../ui";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "~/components/ui/dialog";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export interface PersonalAccessToken {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
obfuscatedToken: string;
|
||||||
|
lastAccessedAt: Date | null;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTokensColumns = (): Array<ColumnDef<PersonalAccessToken>> => {
|
||||||
|
const fetcher = useFetcher();
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const onDelete = (id: string) => {
|
||||||
|
fetcher.submit({ id }, { method: "DELETE", action: "/home/api" });
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
header: () => {
|
||||||
|
return <span>Name</span>;
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<div className="py-2capitalize flex items-center gap-1 py-2">
|
||||||
|
{row.original.name}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "obfuscatedToken",
|
||||||
|
header: () => {
|
||||||
|
return <span>Token</span>;
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1 text-sm">
|
||||||
|
{row.original.obfuscatedToken}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "lastAccessedAt",
|
||||||
|
header: () => {
|
||||||
|
return <span>Last accessed</span>;
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex min-w-[200px] items-center gap-1">
|
||||||
|
{row.original.lastAccessedAt
|
||||||
|
? format(row.original.lastAccessedAt, "MMM d, yyyy")
|
||||||
|
: "Never"}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "actions",
|
||||||
|
header: () => {
|
||||||
|
return <span>Actions</span>;
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Dialog onOpenChange={setOpen} open={open}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="ghost">Delete</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="p-3">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Are you sure?</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
This action cannot be undone. This will permanently delete
|
||||||
|
your API token.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => onDelete(row.original.id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
1
apps/webapp/app/components/api/index.ts
Normal file
1
apps/webapp/app/components/api/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./api-table";
|
||||||
@ -1 +1,2 @@
|
|||||||
export * from "./ingest";
|
export * from "./ingest";
|
||||||
|
export * from "./search";
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon, Loader2 } from "lucide-react";
|
||||||
import { Button } from "../ui";
|
import { Button } from "../ui";
|
||||||
import { Textarea } from "../ui/textarea";
|
import { Textarea } from "../ui/textarea";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { EpisodeType } from "@core/types";
|
import { EpisodeType } from "@core/types";
|
||||||
|
import { useFetcher } from "@remix-run/react";
|
||||||
|
|
||||||
export const IngestBodyRequest = z.object({
|
export const IngestBodyRequest = z.object({
|
||||||
episodeBody: z.string(),
|
episodeBody: z.string(),
|
||||||
@ -16,10 +17,27 @@ export const IngestBodyRequest = z.object({
|
|||||||
|
|
||||||
export const Ingest = () => {
|
export const Ingest = () => {
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
|
const fetcher = useFetcher();
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
fetcher.submit(
|
||||||
|
{
|
||||||
|
episodeBody: text,
|
||||||
|
type: "TEXT",
|
||||||
|
referenceTime: new Date().toISOString(),
|
||||||
|
source: "local",
|
||||||
|
},
|
||||||
|
{ method: "POST", action: "/home/dashboard" },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLoading = fetcher.state === "submitting";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<form method="POST" action="/home/dashboard" className="flex flex-col">
|
<form onSubmit={handleSubmit} className="flex flex-col">
|
||||||
<input type="hidden" name="type" value="TEXT" />
|
<input type="hidden" name="type" value="TEXT" />
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
@ -27,18 +45,27 @@ export const Ingest = () => {
|
|||||||
value={new Date().toISOString()}
|
value={new Date().toISOString()}
|
||||||
/>
|
/>
|
||||||
<input type="hidden" name="source" value="local" />
|
<input type="hidden" name="source" value="local" />
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
name="episodeBody"
|
name="episodeBody"
|
||||||
value={text}
|
value={text}
|
||||||
placeholder="Tell what you want to add"
|
placeholder="Tell what you want to add"
|
||||||
onChange={(e) => setText(e.target.value)}
|
onChange={(e) => setText(e.target.value)}
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-2 flex justify-end">
|
<div className="mt-2 flex justify-end">
|
||||||
<Button type="submit" variant="secondary" className="gap-1">
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="secondary"
|
||||||
|
className="gap-1"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2 size={16} className="animate-spin" />
|
||||||
|
) : (
|
||||||
<PlusIcon size={16} />
|
<PlusIcon size={16} />
|
||||||
Add
|
)}
|
||||||
|
{isLoading ? "Adding..." : "Add"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
98
apps/webapp/app/components/dashboard/search.tsx
Normal file
98
apps/webapp/app/components/dashboard/search.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { PlusIcon, SearchIcon, Loader2 } from "lucide-react";
|
||||||
|
import { Button } from "../ui";
|
||||||
|
import { Textarea } from "../ui/textarea";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { EpisodeType } from "@core/types";
|
||||||
|
import { useFetcher } from "@remix-run/react";
|
||||||
|
|
||||||
|
export const Search = () => {
|
||||||
|
const [text, setText] = useState("");
|
||||||
|
const fetcher = useFetcher<undefined | Record<string, string[]>>();
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
fetcher.submit(
|
||||||
|
{ query: text },
|
||||||
|
{ method: "POST", action: "/home/dashboard" },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchResults = () => {
|
||||||
|
const data = fetcher.data as {
|
||||||
|
episodes?: string[];
|
||||||
|
facts?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!data.episodes || data.episodes.length === 0) &&
|
||||||
|
(!data.facts || data.facts.length === 0)
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div className="mt-4">
|
||||||
|
<p className="text-muted-foreground">No results found</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4">
|
||||||
|
{data.episodes && data.episodes.length > 0 && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="mb-2 text-lg font-semibold">Episodes</h3>
|
||||||
|
{data.episodes.map((episode, index) => (
|
||||||
|
<div key={index} className="bg-secondary mb-2 rounded-lg p-3">
|
||||||
|
{episode}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data.facts && data.facts.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2 text-lg font-semibold">Facts</h3>
|
||||||
|
{data.facts.map((fact, index) => (
|
||||||
|
<div key={index} className="bg-secondary mb-2 rounded-lg p-3">
|
||||||
|
{fact}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLoading = fetcher.state === "submitting";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<form onSubmit={handleSubmit} className="flex flex-col">
|
||||||
|
<Textarea
|
||||||
|
name="query"
|
||||||
|
value={text}
|
||||||
|
placeholder="What do you want to search"
|
||||||
|
onChange={(e) => setText(e.target.value)}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-2 flex justify-end">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="secondary"
|
||||||
|
className="gap-1"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2 size={16} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<SearchIcon size={16} />
|
||||||
|
)}
|
||||||
|
{isLoading ? "Searching..." : "Search"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{fetcher?.data && searchResults()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -114,31 +114,8 @@ export function GraphPopovers({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<p className="text-muted-foreground text-sm break-all">
|
|
||||||
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
|
||||||
Name:
|
|
||||||
</span>
|
|
||||||
{nodePopupContent?.node.name || "Unknown"}
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground text-sm break-words">
|
|
||||||
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
|
||||||
UUID:
|
|
||||||
</span>
|
|
||||||
{nodePopupContent?.node.uuid || "Unknown"}
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground text-sm break-words">
|
|
||||||
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
|
||||||
Created:
|
|
||||||
</span>
|
|
||||||
{nodePopupContent?.node.created_at &&
|
|
||||||
formatDate(nodePopupContent?.node.created_at)}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{attributesToDisplay.length > 0 && (
|
{attributesToDisplay.length > 0 && (
|
||||||
<div className="border-border border-t pt-2">
|
<div>
|
||||||
<p className="mb-2 text-sm font-medium text-black dark:text-white">
|
|
||||||
Properties:
|
|
||||||
</p>
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
{attributesToDisplay.map(({ key, value }) => (
|
{attributesToDisplay.map(({ key, value }) => (
|
||||||
<p key={key} className="text-sm">
|
<p key={key} className="text-sm">
|
||||||
@ -155,33 +132,6 @@ export function GraphPopovers({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{nodePopupContent?.node.summary && (
|
|
||||||
<div className="border-border border-t pt-2">
|
|
||||||
<p className="mb-1 text-sm font-medium text-black dark:text-white">
|
|
||||||
Summary:
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
className="relative max-h-[200px] overflow-y-auto"
|
|
||||||
style={{
|
|
||||||
scrollbarWidth: "thin",
|
|
||||||
scrollbarColor: "rgba(155, 155, 155, 0.5) transparent",
|
|
||||||
pointerEvents: "auto",
|
|
||||||
touchAction: "auto",
|
|
||||||
WebkitOverflowScrolling: "touch",
|
|
||||||
}}
|
|
||||||
onWheel={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const target = e.currentTarget;
|
|
||||||
target.scrollTop += e.deltaY;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p className="text-muted-foreground pr-4 text-sm break-words">
|
|
||||||
{nodePopupContent.node.summary}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
@ -200,11 +150,7 @@ export function GraphPopovers({
|
|||||||
>
|
>
|
||||||
<div className="bg-grayAlpha-100 mb-4 rounded-md p-2">
|
<div className="bg-grayAlpha-100 mb-4 rounded-md p-2">
|
||||||
<p className="text-sm break-all">
|
<p className="text-sm break-all">
|
||||||
{edgePopupContent?.source.name || "Unknown"} →{" "}
|
Episode → {edgePopupContent?.target.name || "Unknown"}
|
||||||
<span className="font-medium">
|
|
||||||
{edgePopupContent?.relation.name || "Unknown"}
|
|
||||||
</span>{" "}
|
|
||||||
→ {edgePopupContent?.target.name || "Unknown"}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@ -220,63 +166,14 @@ export function GraphPopovers({
|
|||||||
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
||||||
Type:
|
Type:
|
||||||
</span>
|
</span>
|
||||||
{edgePopupContent?.relation.name || "Unknown"}
|
{edgePopupContent?.relation.type || "Unknown"}
|
||||||
</p>
|
</p>
|
||||||
{edgePopupContent?.relation.fact && (
|
|
||||||
<p className="text-muted-foreground text-sm break-all">
|
|
||||||
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
|
||||||
Fact:
|
|
||||||
</span>
|
|
||||||
{edgePopupContent.relation.fact}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{edgePopupContent?.relation.episodes?.length ? (
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-black dark:text-white">
|
|
||||||
Episodes:
|
|
||||||
</p>
|
|
||||||
<div className="mt-1 flex gap-2">
|
|
||||||
{edgePopupContent.relation.episodes.map((episode) => (
|
|
||||||
<span
|
|
||||||
key={episode}
|
|
||||||
className="bg-muted rounded-md px-2 py-1 text-xs"
|
|
||||||
>
|
|
||||||
{episode}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<p className="text-muted-foreground text-sm break-all">
|
<p className="text-muted-foreground text-sm break-all">
|
||||||
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
||||||
Created:
|
Created:
|
||||||
</span>
|
</span>
|
||||||
{formatDate(edgePopupContent?.relation.created_at)}
|
{formatDate(edgePopupContent?.relation.createdAt)}
|
||||||
</p>
|
</p>
|
||||||
{edgePopupContent?.relation.valid_at && (
|
|
||||||
<p className="text-muted-foreground text-sm break-all">
|
|
||||||
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
|
||||||
Valid From:
|
|
||||||
</span>
|
|
||||||
{formatDate(edgePopupContent.relation.valid_at)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{edgePopupContent?.relation.expired_at && (
|
|
||||||
<p className="text-muted-foreground text-sm break-all">
|
|
||||||
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
|
||||||
Expired At:
|
|
||||||
</span>
|
|
||||||
{formatDate(edgePopupContent.relation.expired_at)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{edgePopupContent?.relation.invalid_at && (
|
|
||||||
<p className="text-muted-foreground text-sm break-all">
|
|
||||||
<span className="mr-2 text-sm font-medium text-black dark:text-white">
|
|
||||||
Invalid At:
|
|
||||||
</span>
|
|
||||||
{formatDate(edgePopupContent.relation.invalid_at)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
|
|||||||
@ -4,8 +4,7 @@ export interface Node {
|
|||||||
summary?: string;
|
summary?: string;
|
||||||
labels?: string[];
|
labels?: string[];
|
||||||
attributes?: Record<string, any>;
|
attributes?: Record<string, any>;
|
||||||
created_at: string;
|
createdAt: string;
|
||||||
updated_at: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Edge {
|
export interface Edge {
|
||||||
@ -13,14 +12,7 @@ export interface Edge {
|
|||||||
source_node_uuid: string;
|
source_node_uuid: string;
|
||||||
target_node_uuid: string;
|
target_node_uuid: string;
|
||||||
type: string;
|
type: string;
|
||||||
name: string;
|
createdAt: string;
|
||||||
fact?: string;
|
|
||||||
episodes?: string[];
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
valid_at?: string;
|
|
||||||
expired_at?: string;
|
|
||||||
invalid_at?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RawTriplet {
|
export interface RawTriplet {
|
||||||
|
|||||||
@ -16,8 +16,7 @@ export function toGraphNode(node: Node): GraphNode {
|
|||||||
value: node.name,
|
value: node.name,
|
||||||
uuid: node.uuid,
|
uuid: node.uuid,
|
||||||
name: node.name,
|
name: node.name,
|
||||||
created_at: node.created_at,
|
createdAt: node.createdAt,
|
||||||
updated_at: node.updated_at,
|
|
||||||
attributes: node.attributes,
|
attributes: node.attributes,
|
||||||
summary: node.summary,
|
summary: node.summary,
|
||||||
labels: node.labels,
|
labels: node.labels,
|
||||||
@ -28,7 +27,7 @@ export function toGraphNode(node: Node): GraphNode {
|
|||||||
export function toGraphEdge(edge: Edge): GraphEdge {
|
export function toGraphEdge(edge: Edge): GraphEdge {
|
||||||
return {
|
return {
|
||||||
id: edge.uuid,
|
id: edge.uuid,
|
||||||
value: edge.name,
|
value: edge.type,
|
||||||
...edge,
|
...edge,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -90,9 +89,8 @@ export function createTriplets(edges: Edge[], nodes: Node[]): RawTriplet[] {
|
|||||||
target_node_uuid: node.uuid,
|
target_node_uuid: node.uuid,
|
||||||
// Use a special type that we can filter out in the Graph component
|
// Use a special type that we can filter out in the Graph component
|
||||||
type: "_isolated_node_",
|
type: "_isolated_node_",
|
||||||
name: "", // Empty name so it doesn't show a label
|
|
||||||
created_at: node.created_at,
|
createdAt: node.createdAt,
|
||||||
updated_at: node.updated_at,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
1
apps/webapp/app/components/logs/index.ts
Normal file
1
apps/webapp/app/components/logs/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./ingestion-logs-table";
|
||||||
98
apps/webapp/app/components/logs/ingestion-logs-table.tsx
Normal file
98
apps/webapp/app/components/logs/ingestion-logs-table.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import {
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
type ColumnDef,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "../ui/table";
|
||||||
|
|
||||||
|
// Define the type for your ingestion log
|
||||||
|
export type IngestionLog = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
// Add other fields as needed
|
||||||
|
};
|
||||||
|
|
||||||
|
const useIngestionLogsColumns = (): ColumnDef<IngestionLog>[] => [
|
||||||
|
{
|
||||||
|
accessorKey: "id",
|
||||||
|
header: "ID",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "createdAt",
|
||||||
|
header: "Created At",
|
||||||
|
cell: (info) => new Date(info.getValue() as string).toLocaleString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "status",
|
||||||
|
header: "Status",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
},
|
||||||
|
// Add more columns as needed
|
||||||
|
];
|
||||||
|
|
||||||
|
export const IngestionLogsTable = ({
|
||||||
|
ingestionLogs,
|
||||||
|
}: {
|
||||||
|
ingestionLogs: IngestionLog[];
|
||||||
|
}) => {
|
||||||
|
const columns = useIngestionLogsColumns();
|
||||||
|
const table = useReactTable({
|
||||||
|
data: ingestionLogs,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<TableHead key={header.id} className="text-sm">
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
data-state={row.getIsSelected() && "selected"}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id} className="w-[90%] py-2">
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
|
No logs found.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -6,33 +6,32 @@ import {
|
|||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from "../ui/sidebar";
|
} from "../ui/sidebar";
|
||||||
import { DashboardIcon } from "@radix-ui/react-icons";
|
import { DashboardIcon } from "@radix-ui/react-icons";
|
||||||
import { Code, LucideFileStack } from "lucide-react";
|
import { Code, Search } from "lucide-react";
|
||||||
import { NavMain } from "./nav-main";
|
import { NavMain } from "./nav-main";
|
||||||
import { useUser } from "~/hooks/useUser";
|
import { useUser } from "~/hooks/useUser";
|
||||||
import { NavUser } from "./nav-user";
|
import { NavUser } from "./nav-user";
|
||||||
import { useWorkspace } from "~/hooks/useWorkspace";
|
import { useWorkspace } from "~/hooks/useWorkspace";
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
user: {
|
|
||||||
name: "shadcn",
|
|
||||||
email: "m@example.com",
|
|
||||||
avatar: "/avatars/shadcn.jpg",
|
|
||||||
},
|
|
||||||
navMain: [
|
navMain: [
|
||||||
{
|
{
|
||||||
title: "Dashboard",
|
title: "Dashboard",
|
||||||
url: "/",
|
url: "/home/dashboard",
|
||||||
icon: DashboardIcon,
|
icon: DashboardIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "API",
|
title: "API",
|
||||||
url: "/api",
|
url: "/home/api",
|
||||||
icon: Code,
|
icon: Code,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Logs",
|
||||||
|
url: "/home/logs",
|
||||||
|
icon: Search,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -45,16 +44,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton
|
<span className="text-base font-semibold">{workspace.name}</span>
|
||||||
asChild
|
|
||||||
className="data-[slot=sidebar-menu-button]:!p-1.5"
|
|
||||||
>
|
|
||||||
<a href="#">
|
|
||||||
<span className="text-base font-semibold">
|
|
||||||
{workspace.name}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
@ -62,7 +52,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
<NavMain items={data.navMain} />
|
<NavMain items={data.navMain} />
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|
||||||
<SidebarFooter>
|
<SidebarFooter className="p-0">
|
||||||
<NavUser user={user} />
|
<NavUser user={user} />
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|||||||
@ -1,14 +1,11 @@
|
|||||||
import { useUser } from "~/hooks/useUser";
|
|
||||||
import {
|
import {
|
||||||
SidebarFooter,
|
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupContent,
|
SidebarGroupContent,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from "../ui/sidebar";
|
} from "../ui/sidebar";
|
||||||
import { NavUser } from "./nav-user";
|
import { useLocation, useNavigate } from "@remix-run/react";
|
||||||
import { useLocation } from "@remix-run/react";
|
|
||||||
|
|
||||||
export const NavMain = ({
|
export const NavMain = ({
|
||||||
items,
|
items,
|
||||||
@ -20,6 +17,7 @@ export const NavMain = ({
|
|||||||
}[];
|
}[];
|
||||||
}) => {
|
}) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
@ -30,6 +28,7 @@ export const NavMain = ({
|
|||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
tooltip={item.title}
|
tooltip={item.title}
|
||||||
isActive={location.pathname.includes(item.url)}
|
isActive={location.pathname.includes(item.url)}
|
||||||
|
onClick={() => navigate(item.url)}
|
||||||
>
|
>
|
||||||
{item.icon && <item.icon />}
|
{item.icon && <item.icon />}
|
||||||
<span>{item.title}</span>
|
<span>{item.title}</span>
|
||||||
|
|||||||
@ -1,22 +1,16 @@
|
|||||||
import { DotIcon, LogOut, User as UserI } from "lucide-react";
|
import { LogOut } from "lucide-react";
|
||||||
import { Avatar, AvatarFallback, AvatarImage, AvatarText } from "../ui/avatar";
|
import { AvatarText } from "../ui/avatar";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "../ui/dropdown-menu";
|
} from "../ui/dropdown-menu";
|
||||||
import {
|
import { SidebarMenu, SidebarMenuItem, useSidebar } from "../ui/sidebar";
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
useSidebar,
|
|
||||||
} from "../ui/sidebar";
|
|
||||||
import type { User } from "~/models/user.server";
|
import type { User } from "~/models/user.server";
|
||||||
import { useUser } from "~/hooks/useUser";
|
import { Button } from "../ui";
|
||||||
|
|
||||||
export function NavUser({ user }: { user: User }) {
|
export function NavUser({ user }: { user: User }) {
|
||||||
const { isMobile } = useSidebar();
|
const { isMobile } = useSidebar();
|
||||||
@ -26,9 +20,10 @@ export function NavUser({ user }: { user: User }) {
|
|||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuButton
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
variant="link"
|
||||||
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground ,b-2 mb-2 gap-2 px-2"
|
||||||
>
|
>
|
||||||
<AvatarText
|
<AvatarText
|
||||||
text={user.name ?? "User"}
|
text={user.name ?? "User"}
|
||||||
@ -40,21 +35,16 @@ export function NavUser({ user }: { user: User }) {
|
|||||||
{user.email}
|
{user.email}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</SidebarMenuButton>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||||
side={isMobile ? "bottom" : "right"}
|
side={isMobile ? "bottom" : "top"}
|
||||||
align="end"
|
align="end"
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
>
|
>
|
||||||
<DropdownMenuLabel className="p-0 font-normal">
|
<DropdownMenuLabel className="p-0 font-normal">
|
||||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||||
<Avatar className="h-8 w-8 rounded-lg">
|
|
||||||
<AvatarFallback className="rounded-lg">
|
|
||||||
{user.name}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
<span className="truncate font-medium">{user.name}</span>
|
<span className="truncate font-medium">{user.name}</span>
|
||||||
<span className="text-muted-foreground truncate text-xs">
|
<span className="text-muted-foreground truncate text-xs">
|
||||||
@ -64,15 +54,11 @@ export function NavUser({ user }: { user: User }) {
|
|||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuItem
|
||||||
<DropdownMenuItem>
|
className="flex gap-2"
|
||||||
<UserI />
|
onClick={() => (window.location.href = "/logout")}
|
||||||
Account
|
>
|
||||||
</DropdownMenuItem>
|
<LogOut size={16} />
|
||||||
</DropdownMenuGroup>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<LogOut />
|
|
||||||
Log out
|
Log out
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
63
apps/webapp/app/components/ui/alert.tsx
Normal file
63
apps/webapp/app/components/ui/alert.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-background text-foreground",
|
||||||
|
success:
|
||||||
|
"border-success/50 text-success dark:border-success [&>svg]:text-success bg-success/10",
|
||||||
|
warning:
|
||||||
|
"border-warning/50 text-warning dark:border-warning [&>svg]:text-warning bg-warning/10",
|
||||||
|
destructive:
|
||||||
|
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive bg-destructive/10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const Alert = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||||
|
>(({ className, variant, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Alert.displayName = "Alert";
|
||||||
|
|
||||||
|
const AlertTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h5
|
||||||
|
ref={ref}
|
||||||
|
className={cn("mb-1 leading-none font-medium tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AlertTitle.displayName = "AlertTitle";
|
||||||
|
|
||||||
|
const AlertDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AlertDescription.displayName = "AlertDescription";
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription };
|
||||||
132
apps/webapp/app/components/ui/dialog.tsx
Normal file
132
apps/webapp/app/components/ui/dialog.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
|
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root;
|
||||||
|
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger;
|
||||||
|
|
||||||
|
const DialogPortal = DialogPrimitive.Portal;
|
||||||
|
|
||||||
|
const DialogClose = DialogPrimitive.Close;
|
||||||
|
|
||||||
|
const DialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"bg-grayAlpha-300 fixed inset-0 overflow-auto font-sans",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
|
interface DialogContentProps
|
||||||
|
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
|
||||||
|
closeIcon?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
DialogContentProps
|
||||||
|
>(({ className, children, closeIcon = true, ...props }, ref) => (
|
||||||
|
<DialogPortal>
|
||||||
|
<div className="fixed top-0 left-0 z-20 h-[100vh] w-[100vw]">
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-full !w-[100vw] !max-w-[100%] items-start justify-center p-[calc(0.07px_+_13vh)_12px_13vh] font-sans duration-200",
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"bg-background-2 shadow-1 border-border z-50 flex max-h-full min-w-[500px] flex-col gap-4 overflow-hidden sm:rounded-lg",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{closeIcon && (
|
||||||
|
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none">
|
||||||
|
<Cross2Icon className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</div>
|
||||||
|
</DialogPortal>
|
||||||
|
));
|
||||||
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const DialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-1.5 text-center font-sans sm:text-left",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
DialogHeader.displayName = "DialogHeader";
|
||||||
|
|
||||||
|
const DialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse font-sans sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
DialogFooter.displayName = "DialogFooter";
|
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("font-sans text-lg leading-none tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||||
|
|
||||||
|
const DialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-muted-foreground font-sans text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogPortal,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
};
|
||||||
127
apps/webapp/app/components/ui/pagination.tsx
Normal file
127
apps/webapp/app/components/ui/pagination.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils"
|
||||||
|
import { Button, buttonVariants } from "~/components/ui/button"
|
||||||
|
|
||||||
|
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
role="navigation"
|
||||||
|
aria-label="pagination"
|
||||||
|
data-slot="pagination"
|
||||||
|
className={cn("mx-auto flex w-full justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"ul">) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
data-slot="pagination-content"
|
||||||
|
className={cn("flex flex-row items-center gap-1", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||||
|
return <li data-slot="pagination-item" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginationLinkProps = {
|
||||||
|
isActive?: boolean
|
||||||
|
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||||
|
React.ComponentProps<"a">
|
||||||
|
|
||||||
|
function PaginationLink({
|
||||||
|
className,
|
||||||
|
isActive,
|
||||||
|
size = "icon",
|
||||||
|
...props
|
||||||
|
}: PaginationLinkProps) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-current={isActive ? "page" : undefined}
|
||||||
|
data-slot="pagination-link"
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: isActive ? "outline" : "ghost",
|
||||||
|
size,
|
||||||
|
}),
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationPrevious({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to previous page"
|
||||||
|
size="default"
|
||||||
|
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
<span className="hidden sm:block">Previous</span>
|
||||||
|
</PaginationLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationNext({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to next page"
|
||||||
|
size="default"
|
||||||
|
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="hidden sm:block">Next</span>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</PaginationLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationEllipsis({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
data-slot="pagination-ellipsis"
|
||||||
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontalIcon className="size-4" />
|
||||||
|
<span className="sr-only">More pages</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationPrevious,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationEllipsis,
|
||||||
|
}
|
||||||
125
apps/webapp/app/components/ui/table.tsx
Normal file
125
apps/webapp/app/components/ui/table.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
|
const Table = React.forwardRef<
|
||||||
|
HTMLTableElement,
|
||||||
|
React.HTMLAttributes<HTMLTableElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<table
|
||||||
|
ref={ref}
|
||||||
|
className={cn("w-full caption-bottom", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Table.displayName = "Table";
|
||||||
|
|
||||||
|
const TableHeader = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<thead
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"[&_tr]:border-border text-muted-foreground font-mono [&_tr]:border-b",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableHeader.displayName = "TableHeader";
|
||||||
|
|
||||||
|
const TableBody = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tbody
|
||||||
|
ref={ref}
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableBody.displayName = "TableBody";
|
||||||
|
|
||||||
|
const TableFooter = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tfoot
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-border bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableFooter.displayName = "TableFooter";
|
||||||
|
|
||||||
|
const TableRow = React.forwardRef<
|
||||||
|
HTMLTableRowElement,
|
||||||
|
React.HTMLAttributes<HTMLTableRowElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tr
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-border hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableRow.displayName = "TableRow";
|
||||||
|
|
||||||
|
const TableHead = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<th
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground h-10 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableHead.displayName = "TableHead";
|
||||||
|
|
||||||
|
const TableCell = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<td
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableCell.displayName = "TableCell";
|
||||||
|
|
||||||
|
const TableCaption = React.forwardRef<
|
||||||
|
HTMLTableCaptionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<caption
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableCaption.displayName = "TableCaption";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
};
|
||||||
@ -40,15 +40,17 @@ async function processUserJob(userId: string, job: any) {
|
|||||||
await prisma.ingestionQueue.update({
|
await prisma.ingestionQueue.update({
|
||||||
where: { id: job.data.queueId },
|
where: { id: job.data.queueId },
|
||||||
data: {
|
data: {
|
||||||
|
output: episodeDetails,
|
||||||
status: IngestionStatus.COMPLETED,
|
status: IngestionStatus.COMPLETED,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// your processing logic
|
// your processing logic
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
await prisma.ingestionQueue.update({
|
await prisma.ingestionQueue.update({
|
||||||
where: { id: job.data.queueId },
|
where: { id: job.data.queueId },
|
||||||
data: {
|
data: {
|
||||||
|
error: err.message,
|
||||||
status: IngestionStatus.FAILED,
|
status: IngestionStatus.FAILED,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -86,12 +88,28 @@ export const addToQueue = async (
|
|||||||
body: z.infer<typeof IngestBodyRequest>,
|
body: z.infer<typeof IngestBodyRequest>,
|
||||||
userId: string,
|
userId: string,
|
||||||
) => {
|
) => {
|
||||||
|
const user = await prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
Workspace: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user?.Workspace?.id) {
|
||||||
|
throw new Error(
|
||||||
|
"Workspace ID is required to create an ingestion queue entry.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const queuePersist = await prisma.ingestionQueue.create({
|
const queuePersist = await prisma.ingestionQueue.create({
|
||||||
data: {
|
data: {
|
||||||
spaceId: body.spaceId,
|
spaceId: body.spaceId ? body.spaceId : null,
|
||||||
data: body,
|
data: body,
|
||||||
status: IngestionStatus.PENDING,
|
status: IngestionStatus.PENDING,
|
||||||
priority: 1,
|
priority: 1,
|
||||||
|
workspaceId: user.Workspace.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -78,25 +78,21 @@ export const getNodeLinks = async (userId: string) => {
|
|||||||
labels: sourceNode.labels,
|
labels: sourceNode.labels,
|
||||||
attributes: sourceNode.properties,
|
attributes: sourceNode.properties,
|
||||||
name: sourceNode.properties.name || "",
|
name: sourceNode.properties.name || "",
|
||||||
created_at: sourceNode.properties.created_at || "",
|
createdAt: sourceNode.properties.createdAt || "",
|
||||||
updated_at: sourceNode.properties.updated_at || "",
|
|
||||||
},
|
},
|
||||||
edge: {
|
edge: {
|
||||||
uuid: edge.identity.toString(),
|
uuid: edge.identity.toString(),
|
||||||
type: edge.type,
|
type: edge.type,
|
||||||
source_node_uuid: sourceNode.identity.toString(),
|
source_node_uuid: sourceNode.identity.toString(),
|
||||||
target_node_uuid: targetNode.identity.toString(),
|
target_node_uuid: targetNode.identity.toString(),
|
||||||
name: edge.properties.name || "",
|
createdAt: edge.properties.createdAt || "",
|
||||||
created_at: edge.properties.created_at || "",
|
|
||||||
updated_at: edge.properties.updated_at || "",
|
|
||||||
},
|
},
|
||||||
targetNode: {
|
targetNode: {
|
||||||
uuid: targetNode.identity.toString(),
|
uuid: targetNode.identity.toString(),
|
||||||
labels: targetNode.labels,
|
labels: targetNode.labels,
|
||||||
attributes: targetNode.properties,
|
attributes: targetNode.properties,
|
||||||
name: targetNode.properties.name || "",
|
name: targetNode.properties.name || "",
|
||||||
created_at: targetNode.properties.created_at || "",
|
createdAt: edge.properties.createdAt || "",
|
||||||
updated_at: targetNode.properties.updated_at || "",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -62,7 +62,6 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
|
|
||||||
export default function ConfirmBasicDetails() {
|
export default function ConfirmBasicDetails() {
|
||||||
const lastSubmission = useActionData<typeof action>();
|
const lastSubmission = useActionData<typeof action>();
|
||||||
const [selectedApps, setSelectedApps] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const [form, fields] = useForm({
|
const [form, fields] = useForm({
|
||||||
lastSubmission: lastSubmission as any,
|
lastSubmission: lastSubmission as any,
|
||||||
|
|||||||
184
apps/webapp/app/routes/home.api.tsx
Normal file
184
apps/webapp/app/routes/home.api.tsx
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import {
|
||||||
|
type LoaderFunctionArgs,
|
||||||
|
type ActionFunctionArgs,
|
||||||
|
} from "@remix-run/server-runtime";
|
||||||
|
import { Plus, Copy } from "lucide-react";
|
||||||
|
import { Button } from "~/components/ui";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "~/components/ui/dialog";
|
||||||
|
import { useFetcher } from "@remix-run/react";
|
||||||
|
import { Input } from "~/components/ui/input";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { parse } from "@conform-to/zod";
|
||||||
|
import { json } from "@remix-run/node";
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
createPersonalAccessToken,
|
||||||
|
getValidPersonalAccessTokens,
|
||||||
|
revokePersonalAccessToken,
|
||||||
|
} from "~/services/personalAccessToken.server";
|
||||||
|
import { requireUserId } from "~/services/session.server";
|
||||||
|
import { useTypedLoaderData } from "remix-typedjson";
|
||||||
|
import { APITable } from "~/components/api";
|
||||||
|
|
||||||
|
export const APIKeyBodyRequest = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const APIKeyDeleteBodyRequest = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function action({ request }: ActionFunctionArgs) {
|
||||||
|
const userId = await requireUserId(request);
|
||||||
|
|
||||||
|
if (request.method === "DELETE") {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const submission = parse(formData, {
|
||||||
|
schema: APIKeyDeleteBodyRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!submission.value || submission.intent !== "submit") {
|
||||||
|
return json(submission);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await revokePersonalAccessToken(submission.value.id);
|
||||||
|
|
||||||
|
return json(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = await request.formData();
|
||||||
|
|
||||||
|
const submission = parse(formData, {
|
||||||
|
schema: APIKeyBodyRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!submission.value || submission.intent !== "submit") {
|
||||||
|
return json(submission);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await createPersonalAccessToken({
|
||||||
|
name: submission.value.name,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
return json(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loader({ request }: LoaderFunctionArgs) {
|
||||||
|
const userId = await requireUserId(request);
|
||||||
|
const personalAccessTokens = await getValidPersonalAccessTokens(userId);
|
||||||
|
|
||||||
|
return personalAccessTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function API() {
|
||||||
|
const personalAccessTokens = useTypedLoaderData<typeof loader>();
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [showToken, setShowToken] = useState(false);
|
||||||
|
const fetcher = useFetcher<{ token: string }>();
|
||||||
|
const isSubmitting = fetcher.state !== "idle";
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
|
||||||
|
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
fetcher.submit({ name }, { method: "POST", action: "/home/api" });
|
||||||
|
setOpen(false);
|
||||||
|
setShowToken(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = (text: string | undefined) => {
|
||||||
|
text && navigator.clipboard.writeText(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="home flex h-full flex-col overflow-y-auto p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1 text-base">
|
||||||
|
<h2 className="text-lg font-semibold">API Keys</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Create and manage API keys to access your data programmatically. API
|
||||||
|
keys allow secure access to your workspace's data and functionality
|
||||||
|
through our REST API.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className="inline-flex items-center justify-center gap-1"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
<Plus size={16} />
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="p-3">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create API Key</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<fetcher.Form
|
||||||
|
method="post"
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
name="name"
|
||||||
|
placeholder="Enter API key name"
|
||||||
|
className="mt-1"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="secondary"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Creating..." : "Create API Key"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</fetcher.Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={showToken} onOpenChange={setShowToken}>
|
||||||
|
<DialogContent className="p-3">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Your New API Key</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Make sure to copy your API key now. You won't be able to see
|
||||||
|
it again!
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2 rounded-md border p-3">
|
||||||
|
<code className="flex-1 text-sm break-all">
|
||||||
|
{fetcher.data?.token}
|
||||||
|
</code>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => copyToClipboard(fetcher.data?.token)}
|
||||||
|
>
|
||||||
|
<Copy size={16} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<APITable personalAccessTokens={personalAccessTokens} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,4 +1,3 @@
|
|||||||
import { useLocalCommonState } from "~/hooks/use-local-state";
|
|
||||||
import {
|
import {
|
||||||
ResizableHandle,
|
ResizableHandle,
|
||||||
ResizablePanel,
|
ResizablePanel,
|
||||||
@ -15,17 +14,34 @@ import {
|
|||||||
type ActionFunctionArgs,
|
type ActionFunctionArgs,
|
||||||
} from "@remix-run/server-runtime";
|
} from "@remix-run/server-runtime";
|
||||||
import { requireUserId } from "~/services/session.server";
|
import { requireUserId } from "~/services/session.server";
|
||||||
import { useActionData } from "@remix-run/react";
|
|
||||||
import { addToQueue, IngestBodyRequest } from "~/lib/ingest.server";
|
import { addToQueue, IngestBodyRequest } from "~/lib/ingest.server";
|
||||||
import { getNodeLinks } from "~/lib/neo4j.server";
|
import { getNodeLinks } from "~/lib/neo4j.server";
|
||||||
import { useTypedLoaderData } from "remix-typedjson";
|
import { useTypedLoaderData } from "remix-typedjson";
|
||||||
|
|
||||||
import { GraphVisualization } from "~/components/graph/graph-visualization";
|
import { GraphVisualization } from "~/components/graph/graph-visualization";
|
||||||
|
import { Search } from "~/components/dashboard";
|
||||||
|
import { SearchBodyRequest } from "./search";
|
||||||
|
import { SearchService } from "~/services/search.server";
|
||||||
|
|
||||||
export async function action({ request }: ActionFunctionArgs) {
|
export async function action({ request }: ActionFunctionArgs) {
|
||||||
const userId = await requireUserId(request);
|
const userId = await requireUserId(request);
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
|
|
||||||
|
// Check if this is a search request by looking for query parameter
|
||||||
|
if (formData.has("query")) {
|
||||||
|
// Handle ingest request
|
||||||
|
const submission = parse(formData, { schema: SearchBodyRequest });
|
||||||
|
const searchService = new SearchService();
|
||||||
|
|
||||||
|
if (!submission.value || submission.intent !== "submit") {
|
||||||
|
return json(submission);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await searchService.search(submission.value.query, userId);
|
||||||
|
return json(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ingest request
|
||||||
const submission = parse(formData, { schema: IngestBodyRequest });
|
const submission = parse(formData, { schema: IngestBodyRequest });
|
||||||
|
|
||||||
if (!submission.value || submission.intent !== "submit") {
|
if (!submission.value || submission.intent !== "submit") {
|
||||||
@ -45,8 +61,6 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const nodeLinks = useTypedLoaderData<typeof loader>();
|
const nodeLinks = useTypedLoaderData<typeof loader>();
|
||||||
|
|
||||||
const actionData = useActionData<typeof action>();
|
|
||||||
|
|
||||||
const [size, setSize] = useState(15);
|
const [size, setSize] = useState(15);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -57,8 +71,8 @@ export default function Dashboard() {
|
|||||||
order={1}
|
order={1}
|
||||||
id="home"
|
id="home"
|
||||||
>
|
>
|
||||||
<div className="home flex h-full flex-col overflow-y-auto p-3">
|
<div className="home flex h-full flex-col overflow-y-auto p-3 text-base">
|
||||||
<h2 className="text-xl"> Graph </h2>
|
<h3 className="text-lg font-medium">Graph</h3>
|
||||||
<p className="text-muted-foreground"> Your memory graph </p>
|
<p className="text-muted-foreground"> Your memory graph </p>
|
||||||
|
|
||||||
<div className="bg-background-3 mt-2 grow rounded">
|
<div className="bg-background-3 mt-2 grow rounded">
|
||||||
@ -86,7 +100,10 @@ export default function Dashboard() {
|
|||||||
<TabsTrigger value="retrieve">Retrieve</TabsTrigger>
|
<TabsTrigger value="retrieve">Retrieve</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="ingest">
|
<TabsContent value="ingest">
|
||||||
<Ingest actionData={actionData} />
|
<Ingest />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="retrieve">
|
||||||
|
<Search />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
|
|||||||
52
apps/webapp/app/routes/home.logs.tsx
Normal file
52
apps/webapp/app/routes/home.logs.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { json } from "@remix-run/node";
|
||||||
|
import { Link, useLoaderData } from "@remix-run/react";
|
||||||
|
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||||
|
import { IngestionLogsTable } from "~/components/logs";
|
||||||
|
import { getIngestionLogs } from "~/services/ingestionLogs.server";
|
||||||
|
import { requireUserId } from "~/services/session.server";
|
||||||
|
|
||||||
|
export async function loader({ request }: LoaderFunctionArgs) {
|
||||||
|
const userId = await requireUserId(request);
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const page = Number(url.searchParams.get("page") || 1);
|
||||||
|
|
||||||
|
const { ingestionLogs, pagination } = await getIngestionLogs(userId, page);
|
||||||
|
|
||||||
|
return json({ ingestionLogs, pagination });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Logs() {
|
||||||
|
const { ingestionLogs, pagination } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="home flex h-full flex-col overflow-y-auto p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1 text-base">
|
||||||
|
<h2 className="text-lg font-semibold">Logs</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
View and monitor your data ingestion logs. These logs show the
|
||||||
|
history of data being loaded into memory, helping you track and
|
||||||
|
debug the ingestion process.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<IngestionLogsTable ingestionLogs={ingestionLogs} />
|
||||||
|
<div className="mt-4">
|
||||||
|
{Array.from({ length: pagination.pages }, (_, i) => (
|
||||||
|
<Link
|
||||||
|
key={i + 1}
|
||||||
|
to={`?page=${i + 1}`}
|
||||||
|
className={`mx-1 rounded border px-2 py-1 ${
|
||||||
|
pagination.currentPage === i + 1
|
||||||
|
? "bg-gray-200 font-bold"
|
||||||
|
: "bg-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
apps/webapp/app/services/ingestionLogs.server.ts
Normal file
46
apps/webapp/app/services/ingestionLogs.server.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { prisma } from "~/db.server";
|
||||||
|
|
||||||
|
export async function getIngestionLogs(
|
||||||
|
userId: string,
|
||||||
|
page: number = 1,
|
||||||
|
limit: number = 10,
|
||||||
|
) {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
Workspace: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const [ingestionLogs, total] = await Promise.all([
|
||||||
|
prisma.ingestionQueue.findMany({
|
||||||
|
where: {
|
||||||
|
workspaceId: user?.Workspace?.id,
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.ingestionQueue.count({
|
||||||
|
where: {
|
||||||
|
workspaceId: user?.Workspace?.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ingestionLogs,
|
||||||
|
pagination: {
|
||||||
|
total,
|
||||||
|
pages: Math.ceil(total / limit),
|
||||||
|
currentPage: page,
|
||||||
|
limit,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -324,6 +324,6 @@
|
|||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground text-base;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -44,6 +44,7 @@
|
|||||||
"@remix-run/server-runtime": "2.16.7",
|
"@remix-run/server-runtime": "2.16.7",
|
||||||
"@remix-run/v1-meta": "^0.1.3",
|
"@remix-run/v1-meta": "^0.1.3",
|
||||||
"@remixicon/react": "^4.2.0",
|
"@remixicon/react": "^4.2.0",
|
||||||
|
"@tanstack/react-table": "^8.13.2",
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
"@tailwindcss/postcss": "^4.1.7",
|
"@tailwindcss/postcss": "^4.1.7",
|
||||||
"ai": "4.3.14",
|
"ai": "4.3.14",
|
||||||
@ -54,6 +55,7 @@
|
|||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"express": "^4.18.1",
|
"express": "^4.18.1",
|
||||||
"ioredis": "^5.6.1",
|
"ioredis": "^5.6.1",
|
||||||
"isbot": "^4.1.0",
|
"isbot": "^4.1.0",
|
||||||
@ -72,6 +74,7 @@
|
|||||||
"remix-typedjson": "0.3.1",
|
"remix-typedjson": "0.3.1",
|
||||||
"remix-utils": "^7.7.0",
|
"remix-utils": "^7.7.0",
|
||||||
"react-resizable-panels": "^1.0.9",
|
"react-resizable-panels": "^1.0.9",
|
||||||
|
"react-virtualized": "^9.22.6",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwind-scrollbar-hide": "^2.0.0",
|
"tailwind-scrollbar-hide": "^2.0.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `workspaceId` to the `IngestionQueue` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "IngestionQueue" ADD COLUMN "output" JSONB,
|
||||||
|
ADD COLUMN "workspaceId" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "IngestionQueue" ADD CONSTRAINT "IngestionQueue_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@ -56,6 +56,7 @@ model Workspace {
|
|||||||
|
|
||||||
userId String? @unique
|
userId String? @unique
|
||||||
user User? @relation(fields: [userId], references: [id])
|
user User? @relation(fields: [userId], references: [id])
|
||||||
|
IngestionQueue IngestionQueue[]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AuthenticationMethod {
|
enum AuthenticationMethod {
|
||||||
@ -174,9 +175,13 @@ model IngestionQueue {
|
|||||||
|
|
||||||
// Queue metadata
|
// Queue metadata
|
||||||
data Json // The actual data to be processed
|
data Json // The actual data to be processed
|
||||||
|
output Json? // The processed output data
|
||||||
status IngestionStatus
|
status IngestionStatus
|
||||||
priority Int @default(0)
|
priority Int @default(0)
|
||||||
|
|
||||||
|
workspaceId String
|
||||||
|
workspace Workspace @relation(fields: [workspaceId], references: [id])
|
||||||
|
|
||||||
// Error handling
|
// Error handling
|
||||||
error String?
|
error String?
|
||||||
retryCount Int @default(0)
|
retryCount Int @default(0)
|
||||||
|
|||||||
69
pnpm-lock.yaml
generated
69
pnpm-lock.yaml
generated
@ -135,6 +135,9 @@ importers:
|
|||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4.1.7
|
specifier: ^4.1.7
|
||||||
version: 4.1.7
|
version: 4.1.7
|
||||||
|
'@tanstack/react-table':
|
||||||
|
specifier: ^8.13.2
|
||||||
|
version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
ai:
|
ai:
|
||||||
specifier: 4.3.14
|
specifier: 4.3.14
|
||||||
version: 4.3.14(react@18.3.1)(zod@3.23.8)
|
version: 4.3.14(react@18.3.1)(zod@3.23.8)
|
||||||
@ -156,6 +159,9 @@ importers:
|
|||||||
d3:
|
d3:
|
||||||
specifier: ^7.9.0
|
specifier: ^7.9.0
|
||||||
version: 7.9.0
|
version: 7.9.0
|
||||||
|
date-fns:
|
||||||
|
specifier: ^4.1.0
|
||||||
|
version: 4.1.0
|
||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.10
|
specifier: ^1.11.10
|
||||||
version: 1.11.13
|
version: 1.11.13
|
||||||
@ -198,6 +204,9 @@ importers:
|
|||||||
react-resizable-panels:
|
react-resizable-panels:
|
||||||
specifier: ^1.0.9
|
specifier: ^1.0.9
|
||||||
version: 1.0.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 1.0.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
react-virtualized:
|
||||||
|
specifier: ^9.22.6
|
||||||
|
version: 9.22.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
remix-auth:
|
remix-auth:
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
@ -2173,6 +2182,17 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^5.2.0 || ^6
|
vite: ^5.2.0 || ^6
|
||||||
|
|
||||||
|
'@tanstack/react-table@8.21.3':
|
||||||
|
resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8'
|
||||||
|
react-dom: '>=16.8'
|
||||||
|
|
||||||
|
'@tanstack/table-core@8.21.3':
|
||||||
|
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
'@testing-library/dom@8.20.1':
|
'@testing-library/dom@8.20.1':
|
||||||
resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==}
|
resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -2969,6 +2989,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
|
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
|
|
||||||
|
clsx@1.2.1:
|
||||||
|
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
clsx@2.1.1:
|
clsx@2.1.1:
|
||||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -3274,6 +3298,9 @@ packages:
|
|||||||
dataloader@1.4.0:
|
dataloader@1.4.0:
|
||||||
resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==}
|
resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==}
|
||||||
|
|
||||||
|
date-fns@4.1.0:
|
||||||
|
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||||
|
|
||||||
dayjs@1.11.13:
|
dayjs@1.11.13:
|
||||||
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
||||||
|
|
||||||
@ -3406,6 +3433,9 @@ packages:
|
|||||||
dom-accessibility-api@0.5.16:
|
dom-accessibility-api@0.5.16:
|
||||||
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
|
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
|
||||||
|
|
||||||
|
dom-helpers@5.2.1:
|
||||||
|
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||||
|
|
||||||
dotenv-cli@7.4.4:
|
dotenv-cli@7.4.4:
|
||||||
resolution: {integrity: sha512-XkBYCG0tPIes+YZr4SpfFv76SQrV/LeCE8CI7JSEMi3VR9MvTihCGTOtbIexD6i2mXF+6px7trb1imVCXSNMDw==}
|
resolution: {integrity: sha512-XkBYCG0tPIes+YZr4SpfFv76SQrV/LeCE8CI7JSEMi3VR9MvTihCGTOtbIexD6i2mXF+6px7trb1imVCXSNMDw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -5635,6 +5665,9 @@ packages:
|
|||||||
react-is@17.0.2:
|
react-is@17.0.2:
|
||||||
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
||||||
|
|
||||||
|
react-lifecycles-compat@3.0.4:
|
||||||
|
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
|
||||||
|
|
||||||
react-refresh@0.14.2:
|
react-refresh@0.14.2:
|
||||||
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -5688,6 +5721,12 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
react-virtualized@9.22.6:
|
||||||
|
resolution: {integrity: sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
react@18.3.1:
|
react@18.3.1:
|
||||||
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -8662,6 +8701,14 @@ snapshots:
|
|||||||
tailwindcss: 4.1.7
|
tailwindcss: 4.1.7
|
||||||
vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0)
|
vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0)
|
||||||
|
|
||||||
|
'@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/table-core': 8.21.3
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
|
||||||
|
'@tanstack/table-core@8.21.3': {}
|
||||||
|
|
||||||
'@testing-library/dom@8.20.1':
|
'@testing-library/dom@8.20.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.27.1
|
'@babel/code-frame': 7.27.1
|
||||||
@ -9612,6 +9659,8 @@ snapshots:
|
|||||||
|
|
||||||
clone@1.0.4: {}
|
clone@1.0.4: {}
|
||||||
|
|
||||||
|
clsx@1.2.1: {}
|
||||||
|
|
||||||
clsx@2.1.1: {}
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
cluster-key-slot@1.1.2: {}
|
cluster-key-slot@1.1.2: {}
|
||||||
@ -9925,6 +9974,8 @@ snapshots:
|
|||||||
|
|
||||||
dataloader@1.4.0: {}
|
dataloader@1.4.0: {}
|
||||||
|
|
||||||
|
date-fns@4.1.0: {}
|
||||||
|
|
||||||
dayjs@1.11.13: {}
|
dayjs@1.11.13: {}
|
||||||
|
|
||||||
debug@2.6.9:
|
debug@2.6.9:
|
||||||
@ -10039,6 +10090,11 @@ snapshots:
|
|||||||
|
|
||||||
dom-accessibility-api@0.5.16: {}
|
dom-accessibility-api@0.5.16: {}
|
||||||
|
|
||||||
|
dom-helpers@5.2.1:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.27.3
|
||||||
|
csstype: 3.1.3
|
||||||
|
|
||||||
dotenv-cli@7.4.4:
|
dotenv-cli@7.4.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
@ -12617,6 +12673,8 @@ snapshots:
|
|||||||
|
|
||||||
react-is@17.0.2: {}
|
react-is@17.0.2: {}
|
||||||
|
|
||||||
|
react-lifecycles-compat@3.0.4: {}
|
||||||
|
|
||||||
react-refresh@0.14.2: {}
|
react-refresh@0.14.2: {}
|
||||||
|
|
||||||
react-remove-scroll-bar@2.3.8(@types/react@18.3.23)(react@18.3.1):
|
react-remove-scroll-bar@2.3.8(@types/react@18.3.23)(react@18.3.1):
|
||||||
@ -12663,6 +12721,17 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.23
|
'@types/react': 18.3.23
|
||||||
|
|
||||||
|
react-virtualized@9.22.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.27.3
|
||||||
|
clsx: 1.2.1
|
||||||
|
dom-helpers: 5.2.1
|
||||||
|
loose-envify: 1.4.0
|
||||||
|
prop-types: 15.8.1
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
react-lifecycles-compat: 3.0.4
|
||||||
|
|
||||||
react@18.3.1:
|
react@18.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user