@lensjs/core 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/copy-front-build.cjs +16 -0
- package/package.json +40 -0
- package/src/abstracts/adapter.ts +41 -0
- package/src/abstracts/store.ts +36 -0
- package/src/context/container.ts +67 -0
- package/src/context/context.ts +9 -0
- package/src/core/api_controller.ts +116 -0
- package/src/core/lens.ts +147 -0
- package/src/core/watcher.ts +6 -0
- package/src/index.ts +11 -0
- package/src/stores/better_sqlite.ts +176 -0
- package/src/stores/index.ts +1 -0
- package/src/types/index.ts +103 -0
- package/src/ui/README.md +69 -0
- package/src/ui/bun.lock +750 -0
- package/src/ui/eslint.config.js +27 -0
- package/src/ui/index.html +13 -0
- package/src/ui/package-lock.json +2953 -0
- package/src/ui/package.json +40 -0
- package/src/ui/public/favicon.ico +0 -0
- package/src/ui/src/App.tsx +40 -0
- package/src/ui/src/components/DetailPanel.tsx +70 -0
- package/src/ui/src/components/JsonViewer.tsx +232 -0
- package/src/ui/src/components/LoadMore.tsx +25 -0
- package/src/ui/src/components/MethodBadge.tsx +19 -0
- package/src/ui/src/components/Modal.tsx +48 -0
- package/src/ui/src/components/StatusCode.tsx +20 -0
- package/src/ui/src/components/Table.tsx +127 -0
- package/src/ui/src/components/layout/DeleteButton.tsx +60 -0
- package/src/ui/src/components/layout/Footer.tsx +12 -0
- package/src/ui/src/components/layout/Header.tsx +40 -0
- package/src/ui/src/components/layout/Layout.tsx +49 -0
- package/src/ui/src/components/layout/LoadingScreen.tsx +14 -0
- package/src/ui/src/components/layout/Sidebar.tsx +67 -0
- package/src/ui/src/components/queryFormatters/MongoViewer.tsx +92 -0
- package/src/ui/src/components/queryFormatters/QueryViewer.tsx +18 -0
- package/src/ui/src/components/queryFormatters/SqlViewer.tsx +105 -0
- package/src/ui/src/components/table/NoData.tsx +26 -0
- package/src/ui/src/components/tabs/TabbedDataViewer.tsx +77 -0
- package/src/ui/src/containers/queries/QueriesContainer.tsx +21 -0
- package/src/ui/src/containers/queries/QueryDetailsContainer.tsx +15 -0
- package/src/ui/src/containers/requests/RequestDetailsContainer.tsx +16 -0
- package/src/ui/src/containers/requests/RequestsContainer.tsx +22 -0
- package/src/ui/src/hooks/useLensApi.ts +92 -0
- package/src/ui/src/hooks/useLoadMore.ts +48 -0
- package/src/ui/src/hooks/useQueries.ts +58 -0
- package/src/ui/src/hooks/useRequests.ts +79 -0
- package/src/ui/src/hooks/useTanstackApi.ts +126 -0
- package/src/ui/src/index.css +78 -0
- package/src/ui/src/interfaces/index.ts +10 -0
- package/src/ui/src/main.tsx +33 -0
- package/src/ui/src/router/Router.ts +11 -0
- package/src/ui/src/router/routes/Loading.tsx +5 -0
- package/src/ui/src/router/routes/index.tsx +85 -0
- package/src/ui/src/types/index.ts +95 -0
- package/src/ui/src/utils/api.ts +7 -0
- package/src/ui/src/utils/context.ts +24 -0
- package/src/ui/src/utils/date.ts +36 -0
- package/src/ui/src/views/queries/QueryDetails.tsx +58 -0
- package/src/ui/src/views/queries/QueryTable.tsx +21 -0
- package/src/ui/src/views/queries/columns.tsx +83 -0
- package/src/ui/src/views/requests/BasicRequestDetails.tsx +82 -0
- package/src/ui/src/views/requests/RequestDetails.tsx +70 -0
- package/src/ui/src/views/requests/RequetsTable.tsx +19 -0
- package/src/ui/src/views/requests/columns.tsx +62 -0
- package/src/ui/src/vite-env.d.ts +1 -0
- package/src/ui/tsconfig.app.json +27 -0
- package/src/ui/tsconfig.json +7 -0
- package/src/ui/tsconfig.node.json +25 -0
- package/src/ui/vite.config.ts +9 -0
- package/src/utils/event_emitter.ts +13 -0
- package/src/utils/index.ts +176 -0
- package/src/watchers/index.ts +2 -0
- package/src/watchers/query_watcher.ts +15 -0
- package/src/watchers/request_watcher.ts +27 -0
- package/tests/core/lens.test.ts +89 -0
- package/tests/stores/better_sqlite.test.ts +168 -0
- package/tests/utils/index.test.ts +182 -0
- package/tests/watchers/query_watcher.test.ts +35 -0
- package/tests/watchers/request_watcher.test.ts +59 -0
- package/tsconfig.json +3 -0
- package/tsup.config.ts +15 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export default function Footer() {
|
|
2
|
+
return (
|
|
3
|
+
<footer className="container my-6">
|
|
4
|
+
<hr className="border-gray-300 dark:border-neutral-700" />
|
|
5
|
+
<div className="py-4 text-center">
|
|
6
|
+
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
|
7
|
+
Made with ❤️ by our team
|
|
8
|
+
</p>
|
|
9
|
+
</div>
|
|
10
|
+
</footer>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Menu, X } from "lucide-react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import { useConfig } from "../../utils/context";
|
|
4
|
+
import DeleteButton from "./DeleteButton";
|
|
5
|
+
import { getRoutesPaths } from "../../router/routes";
|
|
6
|
+
|
|
7
|
+
interface HeaderProps {
|
|
8
|
+
isMobileSidebarOpen: boolean;
|
|
9
|
+
onToggleMobileSidebar: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const Header = ({
|
|
13
|
+
isMobileSidebarOpen,
|
|
14
|
+
onToggleMobileSidebar,
|
|
15
|
+
}: HeaderProps) => {
|
|
16
|
+
const config = useConfig();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<header className="container my-5 flex items-center justify-between gap-4">
|
|
20
|
+
<Link to={getRoutesPaths(config).REQUESTS}>
|
|
21
|
+
<p className="text-2xl font-bold ">{config.appName}</p>
|
|
22
|
+
</Link>
|
|
23
|
+
|
|
24
|
+
<div className="flex items-center gap-2">
|
|
25
|
+
<DeleteButton />
|
|
26
|
+
<button
|
|
27
|
+
onClick={onToggleMobileSidebar}
|
|
28
|
+
className="p-2 rounded-lg hover:bg-gray-200 dark:hover:bg-neutral-700 transition-colors lg:hidden"
|
|
29
|
+
aria-label="Toggle menu"
|
|
30
|
+
>
|
|
31
|
+
<span className="text-gray-700 dark:text-gray-300">
|
|
32
|
+
{isMobileSidebarOpen ? <X size={18} /> : <Menu size={18} />}
|
|
33
|
+
</span>
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
</header>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default Header;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Outlet, useLocation } from "react-router-dom";
|
|
3
|
+
import Footer from "./Footer";
|
|
4
|
+
import Header from "./Header";
|
|
5
|
+
import Sidebar from "./Sidebar";
|
|
6
|
+
|
|
7
|
+
const Layout = () => {
|
|
8
|
+
const location = useLocation();
|
|
9
|
+
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
setIsMobileSidebarOpen(false);
|
|
13
|
+
}, [location.pathname]);
|
|
14
|
+
|
|
15
|
+
const handleToggleMobileSidebar = () => {
|
|
16
|
+
setIsMobileSidebarOpen(!isMobileSidebarOpen);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const handleCloseMobileSidebar = () => {
|
|
20
|
+
setIsMobileSidebarOpen(false);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
{" "}
|
|
26
|
+
<Header
|
|
27
|
+
isMobileSidebarOpen={isMobileSidebarOpen}
|
|
28
|
+
onToggleMobileSidebar={handleToggleMobileSidebar}
|
|
29
|
+
/>
|
|
30
|
+
<hr className="container my-6 border-gray-300 dark:border-neutral-700" />
|
|
31
|
+
<div className="container flex flex-col h-full lg:flex-row gap-8 relative">
|
|
32
|
+
<Sidebar
|
|
33
|
+
isMobileSidebarOpen={isMobileSidebarOpen}
|
|
34
|
+
onCloseMobileSidebar={handleCloseMobileSidebar}
|
|
35
|
+
/>
|
|
36
|
+
|
|
37
|
+
{/* Main content */}
|
|
38
|
+
<div className="w-full min-h-[88vh] flex-1 flex flex-col min-w-0 pb-5 overflow-hidden">
|
|
39
|
+
<div className="flex-1">
|
|
40
|
+
<Outlet />
|
|
41
|
+
</div>
|
|
42
|
+
<Footer />
|
|
43
|
+
</div>
|
|
44
|
+
</div>{" "}
|
|
45
|
+
</>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default Layout;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const LoadingScreen = () => {
|
|
2
|
+
return (
|
|
3
|
+
<div className="flex items-center justify-center h-screen bg-gray-900">
|
|
4
|
+
<div className="relative">
|
|
5
|
+
<div className="w-20 h-20 border-t-4 border-b-4 border-green-500 rounded-full animate-spin"></div>
|
|
6
|
+
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-white font-bold text-lg">
|
|
7
|
+
LENS
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default LoadingScreen;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Link, useLocation } from "react-router-dom";
|
|
2
|
+
import { getSidebarRoutes } from "../../router/routes";
|
|
3
|
+
import { useConfig } from "../../utils/context";
|
|
4
|
+
|
|
5
|
+
interface SidebarProps {
|
|
6
|
+
isMobileSidebarOpen: boolean;
|
|
7
|
+
onCloseMobileSidebar: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const Sidebar = ({
|
|
11
|
+
isMobileSidebarOpen,
|
|
12
|
+
onCloseMobileSidebar,
|
|
13
|
+
}: SidebarProps) => {
|
|
14
|
+
const config = useConfig();
|
|
15
|
+
const location = useLocation();
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
{/* Overlay for mobile */}
|
|
20
|
+
{isMobileSidebarOpen && (
|
|
21
|
+
<div
|
|
22
|
+
className="fixed inset-0 bg-opacity-50 z-10 lg:hidden"
|
|
23
|
+
onClick={onCloseMobileSidebar}
|
|
24
|
+
/>
|
|
25
|
+
)}
|
|
26
|
+
|
|
27
|
+
{/* Sidebar */}
|
|
28
|
+
<aside
|
|
29
|
+
className={`
|
|
30
|
+
fixed lg:sticky top-0 z-20 lg:z-0
|
|
31
|
+
h-full lg:h-auto
|
|
32
|
+
w-3/4 sm:w-64 lg:w-auto min-w-60
|
|
33
|
+
p-4 lg:p-0
|
|
34
|
+
transform transition-transform duration-300 ease-in-out
|
|
35
|
+
${isMobileSidebarOpen ? "translate-x-0 bg-white dark:bg-neutral-900" : "-translate-x-[120%] lg:translate-x-0"}
|
|
36
|
+
lg:bg-transparent
|
|
37
|
+
`}
|
|
38
|
+
>
|
|
39
|
+
<ul className="flex flex-col gap-2 text-gray-700 dark:text-neutral-300">
|
|
40
|
+
{getSidebarRoutes(config).map((route) => {
|
|
41
|
+
const isActive = location.pathname === route.path;
|
|
42
|
+
const Icon = route.icon;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<li key={route.path} className="contents">
|
|
46
|
+
<Link
|
|
47
|
+
to={route.path}
|
|
48
|
+
className={[
|
|
49
|
+
"flex items-center gap-2 rounded-lg px-3 py-2 font-medium transition-colors",
|
|
50
|
+
isActive
|
|
51
|
+
? "bg-green-100 dark:bg-neutral-800 text-gray-900 dark:text-white"
|
|
52
|
+
: "text-gray-600 dark:text-neutral-400 hover:bg-gray-50 dark:hover:bg-neutral-800/50",
|
|
53
|
+
].join(" ")}
|
|
54
|
+
>
|
|
55
|
+
<Icon size={16} />
|
|
56
|
+
{route.label}
|
|
57
|
+
</Link>
|
|
58
|
+
</li>
|
|
59
|
+
);
|
|
60
|
+
})}
|
|
61
|
+
</ul>
|
|
62
|
+
</aside>
|
|
63
|
+
</>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default Sidebar;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from "react";
|
|
4
|
+
import { Check, Copy } from "lucide-react";
|
|
5
|
+
import jsBeautify from "js-beautify";
|
|
6
|
+
|
|
7
|
+
interface MongoViewerProps {
|
|
8
|
+
query: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// MongoDB keywords and operators to highlight
|
|
12
|
+
const MONGO_KEYWORDS = [
|
|
13
|
+
"db", "find", "insertOne", "insertMany", "updateOne", "updateMany",
|
|
14
|
+
"deleteOne", "deleteMany", "aggregate", "project", "match", "group",
|
|
15
|
+
"sort", "limit", "skip", "$match", "$project", "$group", "$sort",
|
|
16
|
+
"$limit", "$skip", "$and", "$or", "$eq", "$ne", "$in", "$nin",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function highlightMongo(query: string) {
|
|
20
|
+
const tokens = query.split(/(\s+|[{}[\]()=,:])/);
|
|
21
|
+
|
|
22
|
+
return tokens.map((token, i) => {
|
|
23
|
+
if (!token.trim()) return <span key={i}>{token}</span>;
|
|
24
|
+
|
|
25
|
+
if (/^['"`].*['"`]$/.test(token)) {
|
|
26
|
+
// Strings
|
|
27
|
+
return <span key={i} className="text-green-600 dark:text-green-400">{token}</span>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (/^\d+\.?\d*$/.test(token)) {
|
|
31
|
+
// Numbers
|
|
32
|
+
return <span key={i} className="text-orange-600 dark:text-orange-400">{token}</span>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (MONGO_KEYWORDS.includes(token)) {
|
|
36
|
+
// Keywords
|
|
37
|
+
return <span key={i} className="text-blue-600 dark:text-blue-400 font-semibold">{token}</span>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return <span key={i}>{token}</span>;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function MongoViewer({ query }: MongoViewerProps) {
|
|
45
|
+
const [copied, setCopied] = useState(false);
|
|
46
|
+
|
|
47
|
+
const formattedQuery = useMemo(() => {
|
|
48
|
+
try {
|
|
49
|
+
return jsBeautify.js(query, {
|
|
50
|
+
indent_size: 2,
|
|
51
|
+
space_in_empty_paren: true,
|
|
52
|
+
max_preserve_newlines: 2,
|
|
53
|
+
});
|
|
54
|
+
} catch {
|
|
55
|
+
return query;
|
|
56
|
+
}
|
|
57
|
+
}, [query]);
|
|
58
|
+
|
|
59
|
+
const copyToClipboard = async () => {
|
|
60
|
+
try {
|
|
61
|
+
await navigator.clipboard.writeText(formattedQuery);
|
|
62
|
+
setCopied(true);
|
|
63
|
+
setTimeout(() => setCopied(false), 2000);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error("Failed to copy: ", err);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const highlightedQuery = useMemo(() => highlightMongo(formattedQuery), [formattedQuery]);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="bg-neutral-50 dark:bg-slate-900 rounded-lg p-4 font-mono text-sm overflow-x-auto relative">
|
|
73
|
+
<button
|
|
74
|
+
onClick={copyToClipboard}
|
|
75
|
+
className={`absolute top-3 right-3 p-2 rounded-md transition-colors ${
|
|
76
|
+
copied
|
|
77
|
+
? "bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400"
|
|
78
|
+
: "bg-white text-neutral-500 hover:text-neutral-700 hover:bg-neutral-100 dark:bg-slate-800 dark:text-neutral-400 dark:hover:text-neutral-300 dark:hover:bg-slate-700"
|
|
79
|
+
}`}
|
|
80
|
+
title={copied ? "Copied!" : "Copy to clipboard"}
|
|
81
|
+
>
|
|
82
|
+
{copied ? <Check size={16} /> : <Copy size={16} />}
|
|
83
|
+
</button>
|
|
84
|
+
|
|
85
|
+
<pre className="whitespace-pre-wrap pr-12 text-neutral-800 dark:text-neutral-200">
|
|
86
|
+
{highlightedQuery}
|
|
87
|
+
</pre>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default MongoViewer;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { QueryEntry } from "../../types";
|
|
2
|
+
import SqlViewer from "./SqlViewer";
|
|
3
|
+
import MongbDbViewer from "./MongoViewer";
|
|
4
|
+
|
|
5
|
+
interface QueryFormatterProps {
|
|
6
|
+
queryPayload: QueryEntry["data"];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const QueryViewer = ({ queryPayload }: QueryFormatterProps) => {
|
|
10
|
+
switch (queryPayload.type) {
|
|
11
|
+
case "mongodb":
|
|
12
|
+
return <MongbDbViewer query={queryPayload.query} />;
|
|
13
|
+
default:
|
|
14
|
+
return <SqlViewer sql={queryPayload.query} />;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default QueryViewer;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Check, Copy } from "lucide-react";
|
|
4
|
+
import type React from "react";
|
|
5
|
+
import { useMemo, useState } from "react";
|
|
6
|
+
|
|
7
|
+
// --- Static constants ---
|
|
8
|
+
const KEYWORDS = [
|
|
9
|
+
"SELECT", "FROM", "WHERE", "JOIN", "INNER", "LEFT", "RIGHT", "OUTER", "FULL",
|
|
10
|
+
"ON", "AND", "OR", "NOT", "IN", "EXISTS", "BETWEEN", "LIKE", "IS", "NULL",
|
|
11
|
+
"INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE", "CREATE", "TABLE",
|
|
12
|
+
"ALTER", "DROP", "INDEX", "PRIMARY", "KEY", "FOREIGN", "REFERENCES",
|
|
13
|
+
"CONSTRAINT", "UNIQUE", "CHECK", "DEFAULT", "AUTO_INCREMENT", "IDENTITY",
|
|
14
|
+
"GROUP", "BY", "HAVING", "ORDER", "ASC", "DESC", "LIMIT", "OFFSET", "UNION",
|
|
15
|
+
"ALL", "DISTINCT", "COUNT", "SUM", "AVG", "MIN", "MAX", "CASE", "WHEN", "THEN",
|
|
16
|
+
"ELSE", "END", "IF", "IFNULL", "COALESCE", "CAST", "CONVERT", "SUBSTRING",
|
|
17
|
+
"LENGTH", "UPPER", "LOWER", "TRIM",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const FUNCTIONS = [
|
|
21
|
+
"NOW", "CURRENT_TIMESTAMP", "CURRENT_DATE", "CURRENT_TIME", "DATE", "TIME",
|
|
22
|
+
"YEAR", "MONTH", "DAY", "HOUR", "MINUTE", "SECOND",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const DATA_TYPES = [
|
|
26
|
+
"VARCHAR", "CHAR", "TEXT", "INT", "INTEGER", "BIGINT", "SMALLINT", "TINYINT",
|
|
27
|
+
"DECIMAL", "NUMERIC", "FLOAT", "DOUBLE", "REAL", "BIT", "BOOLEAN", "BOOL",
|
|
28
|
+
"DATE", "TIME", "DATETIME", "TIMESTAMP", "YEAR", "BINARY", "VARBINARY",
|
|
29
|
+
"BLOB", "CLOB", "JSON", "UUID",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// --- Syntax highlighter ---
|
|
33
|
+
function highlightSql(sqlText: string) {
|
|
34
|
+
// Split into words, spaces, punctuation, strings, numbers, comments
|
|
35
|
+
const tokens = sqlText.split(
|
|
36
|
+
/(\s+|[(),;]|'[^']*'|"[^"]*"|\b\d+\.?\d*\b|--[^\n]*|\/\*[\s\S]*?\*\/)/
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return tokens.map((token, index) => {
|
|
40
|
+
const trimmed = token.trim();
|
|
41
|
+
if (!trimmed) return <span key={index}>{token}</span>;
|
|
42
|
+
|
|
43
|
+
if ((token.startsWith("'") && token.endsWith("'")) ||
|
|
44
|
+
(token.startsWith('"') && token.endsWith('"')))
|
|
45
|
+
return <span key={index} className="text-green-600 dark:text-green-400">{token}</span>;
|
|
46
|
+
|
|
47
|
+
if (/^\d+\.?\d*$/.test(trimmed))
|
|
48
|
+
return <span key={index} className="text-orange-600 dark:text-orange-400">{token}</span>;
|
|
49
|
+
|
|
50
|
+
if (token.startsWith("--") || (token.startsWith("/*") && token.endsWith("*/")))
|
|
51
|
+
return <span key={index} className="text-neutral-500 dark:text-neutral-400">{token}</span>;
|
|
52
|
+
|
|
53
|
+
if (KEYWORDS.some(k => k.toLowerCase() === trimmed.toLowerCase()))
|
|
54
|
+
return <span key={index} className="text-blue-600 dark:text-blue-400 font-semibold">{token.toUpperCase()}</span>;
|
|
55
|
+
|
|
56
|
+
if (FUNCTIONS.some(f => f.toLowerCase() === trimmed.toLowerCase()))
|
|
57
|
+
return <span key={index} className="text-purple-600 dark:text-purple-400">{token.toUpperCase()}</span>;
|
|
58
|
+
|
|
59
|
+
if (DATA_TYPES.some(t => t.toLowerCase() === trimmed.toLowerCase()))
|
|
60
|
+
return <span key={index} className="text-indigo-600 dark:text-indigo-400">{token.toUpperCase()}</span>;
|
|
61
|
+
|
|
62
|
+
return <span key={index}>{token}</span>;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface SqlViewerProps {
|
|
67
|
+
sql: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const SqlViewer: React.FC<SqlViewerProps> = ({ sql }) => {
|
|
71
|
+
const [copied, setCopied] = useState(false);
|
|
72
|
+
|
|
73
|
+
const copyToClipboard = async () => {
|
|
74
|
+
try {
|
|
75
|
+
await navigator.clipboard.writeText(sql);
|
|
76
|
+
setCopied(true);
|
|
77
|
+
setTimeout(() => setCopied(false), 2000);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error("Failed to copy: ", err);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const highlightedSql = useMemo(() => highlightSql(sql), [sql]);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className="bg-neutral-50 dark:bg-slate-900 rounded-lg p-4 font-mono text-sm overflow-x-auto relative">
|
|
87
|
+
<button
|
|
88
|
+
onClick={copyToClipboard}
|
|
89
|
+
className={`absolute top-3 right-3 p-2 rounded-md transition-colors ${
|
|
90
|
+
copied
|
|
91
|
+
? "bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400"
|
|
92
|
+
: "bg-white text-neutral-500 hover:text-neutral-700 hover:bg-neutral-100 dark:bg-slate-800 dark:text-neutral-400 dark:hover:text-neutral-300 dark:hover:bg-slate-700"
|
|
93
|
+
}`}
|
|
94
|
+
title={copied ? "Copied!" : "Copy to clipboard"}
|
|
95
|
+
>
|
|
96
|
+
{copied ? <Check size={16} /> : <Copy size={16} />}
|
|
97
|
+
</button>
|
|
98
|
+
<pre className="whitespace-pre-wrap pr-12 text-neutral-800 dark:text-neutral-200">
|
|
99
|
+
{highlightedSql}
|
|
100
|
+
</pre>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export default SqlViewer;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export default function NoData() {
|
|
2
|
+
return (
|
|
3
|
+
<>
|
|
4
|
+
{" "}
|
|
5
|
+
<div className="flex flex-col items-center justify-center p-12 text-center">
|
|
6
|
+
<svg
|
|
7
|
+
className="w-16 h-16 text-slate-300 dark:text-slate-600 mb-4"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
viewBox="0 0 24 24"
|
|
11
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
12
|
+
>
|
|
13
|
+
<path
|
|
14
|
+
strokeLinecap="round"
|
|
15
|
+
strokeLinejoin="round"
|
|
16
|
+
strokeWidth={2}
|
|
17
|
+
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
18
|
+
/>
|
|
19
|
+
</svg>
|
|
20
|
+
<h3 className="text-lg font-medium text-slate-900 dark:text-slate-200 mb-1">
|
|
21
|
+
No Data found
|
|
22
|
+
</h3>
|
|
23
|
+
</div>
|
|
24
|
+
</>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type React from "react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import JsonViewer from "../JsonViewer";
|
|
6
|
+
|
|
7
|
+
export interface TabbedDataProps {
|
|
8
|
+
tabs: TabItem[];
|
|
9
|
+
title?: string;
|
|
10
|
+
defaultActiveTab?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TabItem {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
data?: Record<string, any>;
|
|
17
|
+
content?: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const TabbedDataViewer: React.FC<TabbedDataProps> = ({
|
|
21
|
+
tabs,
|
|
22
|
+
title,
|
|
23
|
+
defaultActiveTab,
|
|
24
|
+
}) => {
|
|
25
|
+
const [activeTab, setActiveTab] = useState<string>(
|
|
26
|
+
defaultActiveTab || tabs[0]?.id || ""
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="border border-neutral-200 dark:border-neutral-700 rounded-[15px] shadow-sm">
|
|
31
|
+
{/* Header */}
|
|
32
|
+
{title && (
|
|
33
|
+
<div className="px-6 py-4 rounded-t-[15px] border-b border-neutral-200 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-900">
|
|
34
|
+
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
|
35
|
+
{title}
|
|
36
|
+
</h2>
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
|
|
40
|
+
{/* Tabs */}
|
|
41
|
+
<div className="border-b rounded-[15px] border-neutral-200 dark:border-neutral-700">
|
|
42
|
+
<nav className="flex space-x-8 px-6" aria-label="Tabs">
|
|
43
|
+
{tabs.map((tab) => (
|
|
44
|
+
<button
|
|
45
|
+
key={tab.id}
|
|
46
|
+
onClick={() => setActiveTab(tab.id)}
|
|
47
|
+
className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200 ${
|
|
48
|
+
activeTab === tab.id
|
|
49
|
+
? "border-blue-500 text-blue-600 dark:text-blue-400"
|
|
50
|
+
: "border-transparent text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 hover:border-neutral-300 dark:hover:border-neutral-600"
|
|
51
|
+
}`}
|
|
52
|
+
>
|
|
53
|
+
{tab.label}
|
|
54
|
+
</button>
|
|
55
|
+
))}
|
|
56
|
+
</nav>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div className={"p-6 "}>
|
|
60
|
+
{tabs.map((tab) => (
|
|
61
|
+
<div
|
|
62
|
+
key={tab.id}
|
|
63
|
+
className={activeTab === tab.id ? "block" : "hidden"}
|
|
64
|
+
>
|
|
65
|
+
{tab.content ? (
|
|
66
|
+
<div>{tab.content}</div>
|
|
67
|
+
) : (
|
|
68
|
+
<>{tab.data && <JsonViewer data={tab.data} />}</>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default TabbedDataViewer;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { lazy, useEffect } from "react";
|
|
2
|
+
import useQueries from "../../hooks/useQueries";
|
|
3
|
+
import { useLoadMore } from "../../hooks/useLoadMore";
|
|
4
|
+
import type { QueryTableRow } from "../../types";
|
|
5
|
+
|
|
6
|
+
const QueriesTable = lazy(() => import("../../views/queries/QueryTable"));
|
|
7
|
+
const QueriesContainer = () => {
|
|
8
|
+
const { loadMoreRequests, fetchQueries } = useQueries();
|
|
9
|
+
const hasMoreObject = useLoadMore<QueryTableRow>({
|
|
10
|
+
paginatedPage: loadMoreRequests,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
fetchQueries();
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
console.log('data', hasMoreObject.data)
|
|
18
|
+
return <QueriesTable hasMoreObject={hasMoreObject}/>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default QueriesContainer;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useParams } from "react-router-dom";
|
|
3
|
+
import useQueries from "../../hooks/useQueries";
|
|
4
|
+
import QueryDetails from "../../views/queries/QueryDetails";
|
|
5
|
+
|
|
6
|
+
const QueryDetailsContainer = () => {
|
|
7
|
+
const { query, fetchQuery } = useQueries();
|
|
8
|
+
const { id } = useParams();
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
id && fetchQuery(id);
|
|
11
|
+
}, [id]);
|
|
12
|
+
return <div>{query && <QueryDetails query={query} />}</div>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default QueryDetailsContainer;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { lazy } from "react";
|
|
2
|
+
import { useParams } from "react-router-dom";
|
|
3
|
+
import { useRequestById } from "../../hooks/useTanstackApi";
|
|
4
|
+
|
|
5
|
+
const RequestDetailsTable = lazy(
|
|
6
|
+
() => import("../../views/requests/RequestDetails")
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
const RequestDetailsContainer = () => {
|
|
10
|
+
const { id } = useParams();
|
|
11
|
+
const { data } = useRequestById(id as string);
|
|
12
|
+
|
|
13
|
+
return <>{data?.data && <RequestDetailsTable request={data?.data} />}</>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default RequestDetailsContainer;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { lazy, useEffect } from "react";
|
|
2
|
+
import useRequests from "../../hooks/useRequests";
|
|
3
|
+
import { useLoadMore } from "../../hooks/useLoadMore";
|
|
4
|
+
import type { RequestTableRow } from "../../types";
|
|
5
|
+
|
|
6
|
+
const RequestsTableView = lazy(
|
|
7
|
+
() => import("../../views/requests/RequetsTable"),
|
|
8
|
+
);
|
|
9
|
+
const RequestsContainer = () => {
|
|
10
|
+
const { loadMoreRequests, fetchRequests } = useRequests();
|
|
11
|
+
const hasMoreObject = useLoadMore<RequestTableRow>({
|
|
12
|
+
paginatedPage: loadMoreRequests,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
fetchRequests();
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
return <RequestsTableView hasMoreObject={hasMoreObject} />;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default RequestsContainer;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApiResponse,
|
|
3
|
+
GenericLensEntry,
|
|
4
|
+
PaginatorMeta,
|
|
5
|
+
QueryEntry,
|
|
6
|
+
QueryTableRow,
|
|
7
|
+
RequestEntry,
|
|
8
|
+
RequestTableRow,
|
|
9
|
+
} from "../types";
|
|
10
|
+
import { prepareApiUrl } from "../utils/api";
|
|
11
|
+
import { useConfig } from "../utils/context";
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_META: PaginatorMeta = {
|
|
14
|
+
currentPage: 1,
|
|
15
|
+
lastPage: 1,
|
|
16
|
+
total: 0,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const useLensApi = () => {
|
|
20
|
+
const config = useConfig();
|
|
21
|
+
|
|
22
|
+
async function fetchJson<TData>(
|
|
23
|
+
url: string,
|
|
24
|
+
options?: RequestInit
|
|
25
|
+
): Promise<ApiResponse<TData>> {
|
|
26
|
+
const res = await fetch(url, {
|
|
27
|
+
headers: { "Content-Type": "application/json" },
|
|
28
|
+
...options,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
throw new Error(`Failed to fetch: ${url}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return res.json();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const withQueryParams = (
|
|
39
|
+
endpoint: string,
|
|
40
|
+
params?: Record<string, unknown>
|
|
41
|
+
) => {
|
|
42
|
+
const searchParams = new URLSearchParams(
|
|
43
|
+
Object.entries(params || {}).reduce(
|
|
44
|
+
(acc, [key, value]) => {
|
|
45
|
+
if (value !== undefined && value !== null) {
|
|
46
|
+
acc[key] = String(value);
|
|
47
|
+
}
|
|
48
|
+
return acc;
|
|
49
|
+
},
|
|
50
|
+
{} as Record<string, string>
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return `${endpoint}${searchParams.toString() ? `?${searchParams}` : ""}`;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const getAllRequests = async (page?: number) => {
|
|
58
|
+
return fetchJson<RequestTableRow[]>(
|
|
59
|
+
prepareApiUrl(
|
|
60
|
+
withQueryParams(config.api.requests, {
|
|
61
|
+
page,
|
|
62
|
+
})
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const getRequestById = async (id: string) => {
|
|
68
|
+
return fetchJson<GenericLensEntry<RequestEntry>>(
|
|
69
|
+
prepareApiUrl(`${config.api.requests}/${id}`)
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
const getQueries = async (page: number) => {
|
|
73
|
+
return fetchJson<QueryTableRow[]>(
|
|
74
|
+
prepareApiUrl(
|
|
75
|
+
withQueryParams(config.api.queries, {
|
|
76
|
+
page,
|
|
77
|
+
})
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
const getQueryById = async (id: string) => {
|
|
82
|
+
return fetchJson<QueryEntry>(prepareApiUrl(`${config.api.queries}/${id}`));
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
getAllRequests,
|
|
86
|
+
getRequestById,
|
|
87
|
+
getQueries,
|
|
88
|
+
getQueryById,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default useLensApi;
|