@geekmidas/studio 2.0.1 → 10.0.0-alpha.0
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/package.json +8 -5
- package/CHANGELOG.md +0 -60
- package/src/Studio.ts +0 -367
- package/src/__tests__/Studio.spec.ts +0 -447
- package/src/data/DataBrowser.ts +0 -170
- package/src/data/__tests__/DataBrowser.integration.spec.ts +0 -418
- package/src/data/__tests__/filtering.integration.spec.ts +0 -741
- package/src/data/__tests__/introspection.integration.spec.ts +0 -352
- package/src/data/__tests__/pagination.spec.ts +0 -123
- package/src/data/filtering.ts +0 -191
- package/src/data/index.ts +0 -1
- package/src/data/introspection.ts +0 -220
- package/src/data/pagination.ts +0 -33
- package/src/index.ts +0 -29
- package/src/server/__tests__/hono.integration.spec.ts +0 -619
- package/src/server/hono.ts +0 -427
- package/src/types.ts +0 -278
- package/src/ui-assets.ts +0 -37
- package/tsconfig.json +0 -9
- package/tsdown.config.ts +0 -13
- package/ui/CHANGELOG.md +0 -26
- package/ui/index.html +0 -12
- package/ui/node_modules/.bin/tsc +0 -21
- package/ui/node_modules/.bin/tsserver +0 -21
- package/ui/node_modules/.bin/vite +0 -21
- package/ui/package.json +0 -30
- package/ui/src/App.tsx +0 -100
- package/ui/src/api.ts +0 -213
- package/ui/src/components/FilterPanel.tsx +0 -213
- package/ui/src/components/NavRail.tsx +0 -183
- package/ui/src/components/RowDetail.tsx +0 -119
- package/ui/src/components/StudioHeader.tsx +0 -109
- package/ui/src/components/TableList.tsx +0 -58
- package/ui/src/components/TableView.tsx +0 -564
- package/ui/src/main.tsx +0 -10
- package/ui/src/pages/DashboardPage.tsx +0 -500
- package/ui/src/pages/DatabasePage.tsx +0 -226
- package/ui/src/pages/EndpointDetailsPage.tsx +0 -288
- package/ui/src/pages/ExceptionsPage.tsx +0 -268
- package/ui/src/pages/LogsPage.tsx +0 -228
- package/ui/src/pages/MonitoringPage.tsx +0 -46
- package/ui/src/pages/PerformancePage.tsx +0 -307
- package/ui/src/pages/RequestsPage.tsx +0 -379
- package/ui/src/providers/StudioProvider.tsx +0 -194
- package/ui/src/styles.css +0 -105
- package/ui/src/types.ts +0 -174
- package/ui/src/vite-env.d.ts +0 -1
- package/ui/tsconfig.json +0 -21
- package/ui/tsconfig.tsbuildinfo +0 -1
- package/ui/vite.config.ts +0 -12
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { useCallback } from 'react';
|
|
2
|
-
import type { ColumnInfo, FilterConfig } from '../types';
|
|
3
|
-
|
|
4
|
-
interface FilterPanelProps {
|
|
5
|
-
columns: ColumnInfo[];
|
|
6
|
-
filters: FilterConfig[];
|
|
7
|
-
onFiltersChange: (filters: FilterConfig[]) => void;
|
|
8
|
-
onClose: () => void;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const OPERATORS: { value: string; label: string; types?: string[] }[] = [
|
|
12
|
-
{ value: 'eq', label: 'equals' },
|
|
13
|
-
{ value: 'neq', label: 'not equals' },
|
|
14
|
-
{ value: 'gt', label: 'greater than', types: ['number', 'date', 'datetime'] },
|
|
15
|
-
{
|
|
16
|
-
value: 'gte',
|
|
17
|
-
label: 'greater than or equal',
|
|
18
|
-
types: ['number', 'date', 'datetime'],
|
|
19
|
-
},
|
|
20
|
-
{ value: 'lt', label: 'less than', types: ['number', 'date', 'datetime'] },
|
|
21
|
-
{
|
|
22
|
-
value: 'lte',
|
|
23
|
-
label: 'less than or equal',
|
|
24
|
-
types: ['number', 'date', 'datetime'],
|
|
25
|
-
},
|
|
26
|
-
{ value: 'like', label: 'contains', types: ['string'] },
|
|
27
|
-
{ value: 'ilike', label: 'contains (case insensitive)', types: ['string'] },
|
|
28
|
-
{ value: 'is_null', label: 'is null' },
|
|
29
|
-
{ value: 'is_not_null', label: 'is not null' },
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
function getOperatorsForColumn(column: ColumnInfo) {
|
|
33
|
-
return OPERATORS.filter(
|
|
34
|
-
(op) => !op.types || op.types.includes(column.type || 'string'),
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function FilterPanel({
|
|
39
|
-
columns,
|
|
40
|
-
filters,
|
|
41
|
-
onFiltersChange,
|
|
42
|
-
onClose,
|
|
43
|
-
}: FilterPanelProps) {
|
|
44
|
-
const addFilter = useCallback(() => {
|
|
45
|
-
const firstColumn = columns[0];
|
|
46
|
-
if (!firstColumn) return;
|
|
47
|
-
|
|
48
|
-
onFiltersChange([
|
|
49
|
-
...filters,
|
|
50
|
-
{ column: firstColumn.name, operator: 'eq', value: '' },
|
|
51
|
-
]);
|
|
52
|
-
}, [columns, filters, onFiltersChange]);
|
|
53
|
-
|
|
54
|
-
const updateFilter = useCallback(
|
|
55
|
-
(index: number, updates: Partial<FilterConfig>) => {
|
|
56
|
-
const newFilters = [...filters];
|
|
57
|
-
newFilters[index] = { ...newFilters[index], ...updates };
|
|
58
|
-
onFiltersChange(newFilters);
|
|
59
|
-
},
|
|
60
|
-
[filters, onFiltersChange],
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
const removeFilter = useCallback(
|
|
64
|
-
(index: number) => {
|
|
65
|
-
onFiltersChange(filters.filter((_, i) => i !== index));
|
|
66
|
-
},
|
|
67
|
-
[filters, onFiltersChange],
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
const clearAll = useCallback(() => {
|
|
71
|
-
onFiltersChange([]);
|
|
72
|
-
}, [onFiltersChange]);
|
|
73
|
-
|
|
74
|
-
return (
|
|
75
|
-
<div className="filter-panel bg-studio-surface border-b border-studio-border p-4">
|
|
76
|
-
<div className="flex items-center justify-between mb-3">
|
|
77
|
-
<h3 className="text-sm font-medium text-slate-300">Filters</h3>
|
|
78
|
-
<div className="flex items-center gap-2">
|
|
79
|
-
{filters.length > 0 && (
|
|
80
|
-
<button
|
|
81
|
-
onClick={clearAll}
|
|
82
|
-
className="text-xs text-slate-500 hover:text-slate-300 transition-colors"
|
|
83
|
-
>
|
|
84
|
-
Clear all
|
|
85
|
-
</button>
|
|
86
|
-
)}
|
|
87
|
-
<button
|
|
88
|
-
onClick={onClose}
|
|
89
|
-
className="p-1 hover:bg-studio-hover rounded transition-colors"
|
|
90
|
-
>
|
|
91
|
-
<svg
|
|
92
|
-
className="w-4 h-4 text-slate-400"
|
|
93
|
-
fill="none"
|
|
94
|
-
stroke="currentColor"
|
|
95
|
-
viewBox="0 0 24 24"
|
|
96
|
-
>
|
|
97
|
-
<path
|
|
98
|
-
strokeLinecap="round"
|
|
99
|
-
strokeLinejoin="round"
|
|
100
|
-
strokeWidth={2}
|
|
101
|
-
d="M6 18L18 6M6 6l12 12"
|
|
102
|
-
/>
|
|
103
|
-
</svg>
|
|
104
|
-
</button>
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
|
|
108
|
-
<div className="space-y-2">
|
|
109
|
-
{filters.map((filter, index) => {
|
|
110
|
-
const column = columns.find((c) => c.name === filter.column);
|
|
111
|
-
const operators = column ? getOperatorsForColumn(column) : OPERATORS;
|
|
112
|
-
const needsValue = !['is_null', 'is_not_null'].includes(
|
|
113
|
-
filter.operator,
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
return (
|
|
117
|
-
<div key={index} className="flex items-center gap-2">
|
|
118
|
-
{index > 0 && (
|
|
119
|
-
<span className="text-xs text-slate-500 w-8">and</span>
|
|
120
|
-
)}
|
|
121
|
-
{index === 0 && (
|
|
122
|
-
<span className="text-xs text-slate-500 w-8">Where</span>
|
|
123
|
-
)}
|
|
124
|
-
|
|
125
|
-
{/* Column select */}
|
|
126
|
-
<select
|
|
127
|
-
value={filter.column}
|
|
128
|
-
onChange={(e) =>
|
|
129
|
-
updateFilter(index, { column: e.target.value })
|
|
130
|
-
}
|
|
131
|
-
className="select flex-1 min-w-0"
|
|
132
|
-
>
|
|
133
|
-
{columns.map((col) => (
|
|
134
|
-
<option key={col.name} value={col.name}>
|
|
135
|
-
{col.name}
|
|
136
|
-
</option>
|
|
137
|
-
))}
|
|
138
|
-
</select>
|
|
139
|
-
|
|
140
|
-
{/* Operator select */}
|
|
141
|
-
<select
|
|
142
|
-
value={filter.operator}
|
|
143
|
-
onChange={(e) =>
|
|
144
|
-
updateFilter(index, { operator: e.target.value })
|
|
145
|
-
}
|
|
146
|
-
className="select w-48"
|
|
147
|
-
>
|
|
148
|
-
{operators.map((op) => (
|
|
149
|
-
<option key={op.value} value={op.value}>
|
|
150
|
-
{op.label}
|
|
151
|
-
</option>
|
|
152
|
-
))}
|
|
153
|
-
</select>
|
|
154
|
-
|
|
155
|
-
{/* Value input */}
|
|
156
|
-
{needsValue && (
|
|
157
|
-
<input
|
|
158
|
-
type="text"
|
|
159
|
-
value={filter.value}
|
|
160
|
-
onChange={(e) =>
|
|
161
|
-
updateFilter(index, { value: e.target.value })
|
|
162
|
-
}
|
|
163
|
-
placeholder="Enter value..."
|
|
164
|
-
className="input flex-1 min-w-0"
|
|
165
|
-
/>
|
|
166
|
-
)}
|
|
167
|
-
|
|
168
|
-
{/* Remove button */}
|
|
169
|
-
<button
|
|
170
|
-
onClick={() => removeFilter(index)}
|
|
171
|
-
className="p-1.5 hover:bg-studio-hover rounded transition-colors text-slate-500 hover:text-slate-300"
|
|
172
|
-
>
|
|
173
|
-
<svg
|
|
174
|
-
className="w-4 h-4"
|
|
175
|
-
fill="none"
|
|
176
|
-
stroke="currentColor"
|
|
177
|
-
viewBox="0 0 24 24"
|
|
178
|
-
>
|
|
179
|
-
<path
|
|
180
|
-
strokeLinecap="round"
|
|
181
|
-
strokeLinejoin="round"
|
|
182
|
-
strokeWidth={2}
|
|
183
|
-
d="M6 18L18 6M6 6l12 12"
|
|
184
|
-
/>
|
|
185
|
-
</svg>
|
|
186
|
-
</button>
|
|
187
|
-
</div>
|
|
188
|
-
);
|
|
189
|
-
})}
|
|
190
|
-
</div>
|
|
191
|
-
|
|
192
|
-
<button
|
|
193
|
-
onClick={addFilter}
|
|
194
|
-
className="mt-3 flex items-center gap-2 text-sm text-emerald-400 hover:text-emerald-300 transition-colors"
|
|
195
|
-
>
|
|
196
|
-
<svg
|
|
197
|
-
className="w-4 h-4"
|
|
198
|
-
fill="none"
|
|
199
|
-
stroke="currentColor"
|
|
200
|
-
viewBox="0 0 24 24"
|
|
201
|
-
>
|
|
202
|
-
<path
|
|
203
|
-
strokeLinecap="round"
|
|
204
|
-
strokeLinejoin="round"
|
|
205
|
-
strokeWidth={2}
|
|
206
|
-
d="M12 4v16m8-8H4"
|
|
207
|
-
/>
|
|
208
|
-
</svg>
|
|
209
|
-
Add filter
|
|
210
|
-
</button>
|
|
211
|
-
</div>
|
|
212
|
-
);
|
|
213
|
-
}
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Tooltip,
|
|
3
|
-
TooltipContent,
|
|
4
|
-
TooltipProvider,
|
|
5
|
-
TooltipTrigger,
|
|
6
|
-
} from '@geekmidas/ui';
|
|
7
|
-
import { ChevronLeft, ChevronRight, type LucideIcon } from 'lucide-react';
|
|
8
|
-
import { createContext, useContext, useEffect, useState } from 'react';
|
|
9
|
-
import { Link, useLocation } from 'react-router-dom';
|
|
10
|
-
|
|
11
|
-
const STORAGE_KEY = 'studio-nav-collapsed';
|
|
12
|
-
|
|
13
|
-
interface NavRailContextValue {
|
|
14
|
-
collapsed: boolean;
|
|
15
|
-
setCollapsed: (collapsed: boolean) => void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const NavRailContext = createContext<NavRailContextValue>({
|
|
19
|
-
collapsed: false,
|
|
20
|
-
setCollapsed: () => {},
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
export function useNavRail() {
|
|
24
|
-
return useContext(NavRailContext);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface NavRailProps {
|
|
28
|
-
children: React.ReactNode;
|
|
29
|
-
header?: React.ReactNode;
|
|
30
|
-
footer?: React.ReactNode;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function NavRail({ children, header, footer }: NavRailProps) {
|
|
34
|
-
const [collapsed, setCollapsed] = useState(() => {
|
|
35
|
-
if (typeof window !== 'undefined') {
|
|
36
|
-
const stored = localStorage.getItem(STORAGE_KEY);
|
|
37
|
-
return stored === 'true';
|
|
38
|
-
}
|
|
39
|
-
return false;
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
localStorage.setItem(STORAGE_KEY, String(collapsed));
|
|
44
|
-
}, [collapsed]);
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<NavRailContext.Provider value={{ collapsed, setCollapsed }}>
|
|
48
|
-
<TooltipProvider delayDuration={0}>
|
|
49
|
-
<nav
|
|
50
|
-
className={`
|
|
51
|
-
flex flex-col h-full
|
|
52
|
-
bg-[#0d0d0d] border-r border-white/[0.06]
|
|
53
|
-
transition-all duration-200 ease-out
|
|
54
|
-
${collapsed ? 'w-[52px]' : 'w-[180px]'}
|
|
55
|
-
`}
|
|
56
|
-
>
|
|
57
|
-
{/* Toggle button */}
|
|
58
|
-
<div className="flex items-center justify-end h-12 px-2">
|
|
59
|
-
<button
|
|
60
|
-
onClick={() => setCollapsed(!collapsed)}
|
|
61
|
-
className="
|
|
62
|
-
p-1.5 rounded-md
|
|
63
|
-
text-white/40 hover:text-white/70
|
|
64
|
-
hover:bg-white/[0.06]
|
|
65
|
-
transition-colors
|
|
66
|
-
"
|
|
67
|
-
aria-label={
|
|
68
|
-
collapsed ? 'Expand navigation' : 'Collapse navigation'
|
|
69
|
-
}
|
|
70
|
-
>
|
|
71
|
-
{collapsed ? (
|
|
72
|
-
<ChevronRight className="h-4 w-4" />
|
|
73
|
-
) : (
|
|
74
|
-
<ChevronLeft className="h-4 w-4" />
|
|
75
|
-
)}
|
|
76
|
-
</button>
|
|
77
|
-
</div>
|
|
78
|
-
|
|
79
|
-
{header && <div className="px-2 mb-2">{header}</div>}
|
|
80
|
-
|
|
81
|
-
{/* Navigation items */}
|
|
82
|
-
<div className="flex-1 flex flex-col gap-0.5 px-2">{children}</div>
|
|
83
|
-
|
|
84
|
-
{footer && (
|
|
85
|
-
<div className="px-2 py-2 border-t border-white/[0.06]">
|
|
86
|
-
{footer}
|
|
87
|
-
</div>
|
|
88
|
-
)}
|
|
89
|
-
</nav>
|
|
90
|
-
</TooltipProvider>
|
|
91
|
-
</NavRailContext.Provider>
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
interface NavRailItemProps {
|
|
96
|
-
to: string;
|
|
97
|
-
icon: LucideIcon;
|
|
98
|
-
children: React.ReactNode;
|
|
99
|
-
matchPath?: string;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function NavRailItem({
|
|
103
|
-
to,
|
|
104
|
-
icon: Icon,
|
|
105
|
-
children,
|
|
106
|
-
matchPath,
|
|
107
|
-
}: NavRailItemProps) {
|
|
108
|
-
const { collapsed } = useNavRail();
|
|
109
|
-
const location = useLocation();
|
|
110
|
-
|
|
111
|
-
// Special handling for root path
|
|
112
|
-
const isActive = matchPath
|
|
113
|
-
? location.pathname.startsWith(matchPath)
|
|
114
|
-
: to === '/'
|
|
115
|
-
? location.pathname === '/'
|
|
116
|
-
: location.pathname === to || location.pathname.startsWith(`${to}/`);
|
|
117
|
-
|
|
118
|
-
const content = (
|
|
119
|
-
<Link
|
|
120
|
-
to={to}
|
|
121
|
-
className={`
|
|
122
|
-
relative flex items-center gap-3
|
|
123
|
-
px-3 py-2 rounded-md
|
|
124
|
-
text-sm font-medium
|
|
125
|
-
transition-colors duration-150
|
|
126
|
-
${collapsed ? 'justify-center px-0' : ''}
|
|
127
|
-
${
|
|
128
|
-
isActive
|
|
129
|
-
? 'text-white bg-white/[0.08]'
|
|
130
|
-
: 'text-white/60 hover:text-white/90 hover:bg-white/[0.04]'
|
|
131
|
-
}
|
|
132
|
-
`}
|
|
133
|
-
>
|
|
134
|
-
{/* Active indicator bar */}
|
|
135
|
-
{isActive && (
|
|
136
|
-
<span className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-4 bg-emerald-500 rounded-full" />
|
|
137
|
-
)}
|
|
138
|
-
|
|
139
|
-
<Icon
|
|
140
|
-
className={`h-[18px] w-[18px] shrink-0 ${collapsed ? '' : 'ml-0.5'}`}
|
|
141
|
-
/>
|
|
142
|
-
|
|
143
|
-
{!collapsed && <span className="truncate">{children}</span>}
|
|
144
|
-
</Link>
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
if (collapsed) {
|
|
148
|
-
return (
|
|
149
|
-
<Tooltip>
|
|
150
|
-
<TooltipTrigger asChild>{content}</TooltipTrigger>
|
|
151
|
-
<TooltipContent
|
|
152
|
-
side="right"
|
|
153
|
-
sideOffset={8}
|
|
154
|
-
className="bg-[#1a1a1a] text-white border-white/10"
|
|
155
|
-
>
|
|
156
|
-
{children}
|
|
157
|
-
</TooltipContent>
|
|
158
|
-
</Tooltip>
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return content;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
interface NavRailSectionProps {
|
|
166
|
-
title?: string;
|
|
167
|
-
children: React.ReactNode;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export function NavRailSection({ title, children }: NavRailSectionProps) {
|
|
171
|
-
const { collapsed } = useNavRail();
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<div className="flex flex-col gap-0.5">
|
|
175
|
-
{title && !collapsed && (
|
|
176
|
-
<div className="px-3 py-2 text-[11px] font-medium text-white/30 uppercase tracking-wider">
|
|
177
|
-
{title}
|
|
178
|
-
</div>
|
|
179
|
-
)}
|
|
180
|
-
{children}
|
|
181
|
-
</div>
|
|
182
|
-
);
|
|
183
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import type { ColumnInfo } from '../types';
|
|
2
|
-
|
|
3
|
-
interface RowDetailProps {
|
|
4
|
-
row: Record<string, unknown>;
|
|
5
|
-
columns: ColumnInfo[];
|
|
6
|
-
onClose: () => void;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function RowDetail({ row, columns, onClose }: RowDetailProps) {
|
|
10
|
-
const formatValue = (value: unknown): string => {
|
|
11
|
-
if (value === null) return 'NULL';
|
|
12
|
-
if (value === undefined) return '';
|
|
13
|
-
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
14
|
-
if (typeof value === 'object') return JSON.stringify(value, null, 2);
|
|
15
|
-
return String(value);
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const getValueClass = (value: unknown): string => {
|
|
19
|
-
if (value === null) return 'text-slate-500 italic';
|
|
20
|
-
if (typeof value === 'boolean')
|
|
21
|
-
return value ? 'text-emerald-400' : 'text-red-400';
|
|
22
|
-
if (typeof value === 'number') return 'text-blue-400';
|
|
23
|
-
return 'text-slate-300';
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<aside className="w-96 bg-studio-surface border-l border-studio-border flex flex-col shrink-0 overflow-hidden">
|
|
28
|
-
{/* Header */}
|
|
29
|
-
<div className="flex items-center justify-between px-4 py-3 border-b border-studio-border shrink-0">
|
|
30
|
-
<h2 className="text-sm font-medium text-slate-200">Row Details</h2>
|
|
31
|
-
<button
|
|
32
|
-
onClick={onClose}
|
|
33
|
-
className="p-1 hover:bg-studio-hover rounded transition-colors"
|
|
34
|
-
>
|
|
35
|
-
<svg
|
|
36
|
-
className="w-4 h-4 text-slate-400"
|
|
37
|
-
fill="none"
|
|
38
|
-
stroke="currentColor"
|
|
39
|
-
viewBox="0 0 24 24"
|
|
40
|
-
>
|
|
41
|
-
<path
|
|
42
|
-
strokeLinecap="round"
|
|
43
|
-
strokeLinejoin="round"
|
|
44
|
-
strokeWidth={2}
|
|
45
|
-
d="M6 18L18 6M6 6l12 12"
|
|
46
|
-
/>
|
|
47
|
-
</svg>
|
|
48
|
-
</button>
|
|
49
|
-
</div>
|
|
50
|
-
|
|
51
|
-
{/* Content */}
|
|
52
|
-
<div className="flex-1 overflow-y-auto">
|
|
53
|
-
{columns.map((col) => {
|
|
54
|
-
const value = row[col.name];
|
|
55
|
-
const isLongValue = typeof value === 'string' && value.length > 50;
|
|
56
|
-
const isJson = typeof value === 'object' && value !== null;
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<div
|
|
60
|
-
key={col.name}
|
|
61
|
-
className="px-4 py-3 border-b border-studio-border"
|
|
62
|
-
>
|
|
63
|
-
{/* Column name and metadata */}
|
|
64
|
-
<div className="flex items-center gap-2 mb-1.5">
|
|
65
|
-
<span className="text-sm font-medium text-slate-300">
|
|
66
|
-
{col.name}
|
|
67
|
-
</span>
|
|
68
|
-
<span className="text-xs text-slate-600">{col.rawType}</span>
|
|
69
|
-
{col.isPrimaryKey && (
|
|
70
|
-
<span className="text-[10px] px-1 py-0.5 rounded bg-amber-500/20 text-amber-400">
|
|
71
|
-
PK
|
|
72
|
-
</span>
|
|
73
|
-
)}
|
|
74
|
-
{col.isForeignKey && (
|
|
75
|
-
<span className="text-[10px] px-1 py-0.5 rounded bg-blue-500/20 text-blue-400">
|
|
76
|
-
FK
|
|
77
|
-
</span>
|
|
78
|
-
)}
|
|
79
|
-
</div>
|
|
80
|
-
|
|
81
|
-
{/* Value */}
|
|
82
|
-
{isLongValue || isJson ? (
|
|
83
|
-
<pre
|
|
84
|
-
className={`${getValueClass(value)} bg-studio-bg text-xs p-2 rounded overflow-x-auto whitespace-pre-wrap break-words max-h-48`}
|
|
85
|
-
>
|
|
86
|
-
{formatValue(value)}
|
|
87
|
-
</pre>
|
|
88
|
-
) : (
|
|
89
|
-
<div className={`text-sm ${getValueClass(value)}`}>
|
|
90
|
-
{formatValue(value)}
|
|
91
|
-
</div>
|
|
92
|
-
)}
|
|
93
|
-
|
|
94
|
-
{/* Foreign key reference */}
|
|
95
|
-
{col.isForeignKey && col.foreignKeyTable && (
|
|
96
|
-
<div className="mt-1.5 text-xs text-blue-400 flex items-center gap-1">
|
|
97
|
-
<svg
|
|
98
|
-
className="w-3 h-3"
|
|
99
|
-
fill="none"
|
|
100
|
-
stroke="currentColor"
|
|
101
|
-
viewBox="0 0 24 24"
|
|
102
|
-
>
|
|
103
|
-
<path
|
|
104
|
-
strokeLinecap="round"
|
|
105
|
-
strokeLinejoin="round"
|
|
106
|
-
strokeWidth={2}
|
|
107
|
-
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
|
|
108
|
-
/>
|
|
109
|
-
</svg>
|
|
110
|
-
{col.foreignKeyTable}.{col.foreignKeyColumn}
|
|
111
|
-
</div>
|
|
112
|
-
)}
|
|
113
|
-
</div>
|
|
114
|
-
);
|
|
115
|
-
})}
|
|
116
|
-
</div>
|
|
117
|
-
</aside>
|
|
118
|
-
);
|
|
119
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { RefreshCw } from 'lucide-react';
|
|
2
|
-
import { useLocation } from 'react-router-dom';
|
|
3
|
-
import { useStudio } from '../providers/StudioProvider';
|
|
4
|
-
|
|
5
|
-
const PAGE_TITLES: Record<string, string> = {
|
|
6
|
-
'/': 'Dashboard',
|
|
7
|
-
'/database': 'Database',
|
|
8
|
-
'/monitoring': 'Monitoring',
|
|
9
|
-
'/monitoring/requests': 'Requests',
|
|
10
|
-
'/monitoring/logs': 'Logs',
|
|
11
|
-
'/monitoring/exceptions': 'Exceptions',
|
|
12
|
-
'/performance': 'Performance',
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
function getPageTitle(pathname: string): string {
|
|
16
|
-
// Exact match first
|
|
17
|
-
if (PAGE_TITLES[pathname]) {
|
|
18
|
-
return PAGE_TITLES[pathname];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Check for partial matches (e.g., /database/users -> Database)
|
|
22
|
-
for (const [path, title] of Object.entries(PAGE_TITLES)) {
|
|
23
|
-
if (pathname.startsWith(path) && path !== '/') {
|
|
24
|
-
return title;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return 'Studio';
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function StudioHeader() {
|
|
32
|
-
const { connected, stats, loading, refresh } = useStudio();
|
|
33
|
-
const location = useLocation();
|
|
34
|
-
const pageTitle = getPageTitle(location.pathname);
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<header className="flex items-center justify-between h-14 px-6 border-b border-white/[0.06] bg-[#0a0a0a]">
|
|
38
|
-
{/* Page Title */}
|
|
39
|
-
<div className="flex items-center gap-3">
|
|
40
|
-
<h1 className="text-[15px] font-semibold text-white">{pageTitle}</h1>
|
|
41
|
-
</div>
|
|
42
|
-
|
|
43
|
-
{/* Right side: Stats + Actions */}
|
|
44
|
-
<div className="flex items-center gap-6">
|
|
45
|
-
{/* Stats */}
|
|
46
|
-
{stats && (
|
|
47
|
-
<div className="flex items-center gap-5 text-[13px]">
|
|
48
|
-
<div className="flex items-center gap-1.5">
|
|
49
|
-
<span className="text-white/40">Requests</span>
|
|
50
|
-
<span className="font-medium text-white tabular-nums">
|
|
51
|
-
{stats.requests.toLocaleString()}
|
|
52
|
-
</span>
|
|
53
|
-
</div>
|
|
54
|
-
<div className="flex items-center gap-1.5">
|
|
55
|
-
<span className="text-white/40">Exceptions</span>
|
|
56
|
-
<span
|
|
57
|
-
className={`font-medium tabular-nums ${stats.exceptions > 0 ? 'text-red-400' : 'text-white'}`}
|
|
58
|
-
>
|
|
59
|
-
{stats.exceptions.toLocaleString()}
|
|
60
|
-
</span>
|
|
61
|
-
</div>
|
|
62
|
-
<div className="flex items-center gap-1.5">
|
|
63
|
-
<span className="text-white/40">Logs</span>
|
|
64
|
-
<span className="font-medium text-white tabular-nums">
|
|
65
|
-
{stats.logs.toLocaleString()}
|
|
66
|
-
</span>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
)}
|
|
70
|
-
|
|
71
|
-
{/* Divider */}
|
|
72
|
-
<div className="h-4 w-px bg-white/[0.08]" />
|
|
73
|
-
|
|
74
|
-
{/* Refresh Button */}
|
|
75
|
-
<button
|
|
76
|
-
onClick={refresh}
|
|
77
|
-
disabled={loading}
|
|
78
|
-
className="
|
|
79
|
-
flex items-center gap-2 px-3 py-1.5
|
|
80
|
-
text-[13px] font-medium
|
|
81
|
-
text-white/70 hover:text-white
|
|
82
|
-
bg-white/[0.04] hover:bg-white/[0.08]
|
|
83
|
-
border border-white/[0.08] hover:border-white/[0.12]
|
|
84
|
-
rounded-md transition-all
|
|
85
|
-
disabled:opacity-50 disabled:cursor-not-allowed
|
|
86
|
-
"
|
|
87
|
-
>
|
|
88
|
-
<RefreshCw
|
|
89
|
-
className={`h-3.5 w-3.5 ${loading ? 'animate-spin' : ''}`}
|
|
90
|
-
/>
|
|
91
|
-
Refresh
|
|
92
|
-
</button>
|
|
93
|
-
|
|
94
|
-
{/* Connection Status */}
|
|
95
|
-
<div className="flex items-center gap-2">
|
|
96
|
-
<span
|
|
97
|
-
className={`
|
|
98
|
-
h-2 w-2 rounded-full
|
|
99
|
-
${connected ? 'bg-emerald-500' : 'bg-red-500'}
|
|
100
|
-
`}
|
|
101
|
-
/>
|
|
102
|
-
<span className="text-[13px] text-white/50">
|
|
103
|
-
{connected ? 'Live' : 'Disconnected'}
|
|
104
|
-
</span>
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
</header>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import type { TableSummary } from '../types';
|
|
2
|
-
|
|
3
|
-
interface TableListProps {
|
|
4
|
-
tables: TableSummary[];
|
|
5
|
-
selectedTable: string | null;
|
|
6
|
-
onSelect: (tableName: string) => void;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function TableList({ tables, selectedTable, onSelect }: TableListProps) {
|
|
10
|
-
if (tables.length === 0) {
|
|
11
|
-
return (
|
|
12
|
-
<div className="p-4 text-center text-slate-500 text-sm">
|
|
13
|
-
<p>No tables found</p>
|
|
14
|
-
<p className="mt-1 text-xs">Check your database schema.</p>
|
|
15
|
-
</div>
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return (
|
|
20
|
-
<div className="py-1">
|
|
21
|
-
{tables.map((table) => (
|
|
22
|
-
<button
|
|
23
|
-
key={table.name}
|
|
24
|
-
onClick={() => onSelect(table.name)}
|
|
25
|
-
className={`w-full text-left px-3 py-2 flex items-center gap-3 transition-colors ${
|
|
26
|
-
selectedTable === table.name
|
|
27
|
-
? 'bg-studio-hover text-white'
|
|
28
|
-
: 'text-slate-400 hover:bg-studio-hover hover:text-slate-200'
|
|
29
|
-
}`}
|
|
30
|
-
>
|
|
31
|
-
{/* Table icon */}
|
|
32
|
-
<svg
|
|
33
|
-
className={`w-4 h-4 shrink-0 ${selectedTable === table.name ? 'text-emerald-400' : 'text-slate-500'}`}
|
|
34
|
-
fill="none"
|
|
35
|
-
stroke="currentColor"
|
|
36
|
-
viewBox="0 0 24 24"
|
|
37
|
-
>
|
|
38
|
-
<path
|
|
39
|
-
strokeLinecap="round"
|
|
40
|
-
strokeLinejoin="round"
|
|
41
|
-
strokeWidth={1.5}
|
|
42
|
-
d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
|
|
43
|
-
/>
|
|
44
|
-
</svg>
|
|
45
|
-
|
|
46
|
-
<div className="flex-1 min-w-0">
|
|
47
|
-
<div className="truncate text-sm">{table.name}</div>
|
|
48
|
-
{table.estimatedRowCount !== undefined && (
|
|
49
|
-
<div className="text-xs text-slate-500">
|
|
50
|
-
{table.estimatedRowCount.toLocaleString()} rows
|
|
51
|
-
</div>
|
|
52
|
-
)}
|
|
53
|
-
</div>
|
|
54
|
-
</button>
|
|
55
|
-
))}
|
|
56
|
-
</div>
|
|
57
|
-
);
|
|
58
|
-
}
|