datool 0.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.
Files changed (45) hide show
  1. package/README.md +218 -0
  2. package/client-dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  3. package/client-dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  4. package/client-dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  5. package/client-dist/assets/index-BeRNeRUq.css +1 -0
  6. package/client-dist/assets/index-uoZ4c_I8.js +164 -0
  7. package/client-dist/index.html +13 -0
  8. package/index.html +12 -0
  9. package/package.json +55 -0
  10. package/src/client/App.tsx +885 -0
  11. package/src/client/components/connection-status.tsx +43 -0
  12. package/src/client/components/data-table-cell.tsx +235 -0
  13. package/src/client/components/data-table-col-icon.tsx +73 -0
  14. package/src/client/components/data-table-header-col.tsx +225 -0
  15. package/src/client/components/data-table-search-input.tsx +729 -0
  16. package/src/client/components/data-table.tsx +2014 -0
  17. package/src/client/components/stream-controls.tsx +157 -0
  18. package/src/client/components/theme-provider.tsx +230 -0
  19. package/src/client/components/ui/button.tsx +68 -0
  20. package/src/client/components/ui/combobox.tsx +308 -0
  21. package/src/client/components/ui/context-menu.tsx +261 -0
  22. package/src/client/components/ui/dropdown-menu.tsx +267 -0
  23. package/src/client/components/ui/input-group.tsx +153 -0
  24. package/src/client/components/ui/input.tsx +19 -0
  25. package/src/client/components/ui/textarea.tsx +18 -0
  26. package/src/client/components/viewer-settings.tsx +185 -0
  27. package/src/client/index.css +192 -0
  28. package/src/client/lib/data-table-search.ts +750 -0
  29. package/src/client/lib/datool-icons.ts +37 -0
  30. package/src/client/lib/datool-url-state.ts +159 -0
  31. package/src/client/lib/filterable-table.ts +146 -0
  32. package/src/client/lib/table-search-persistence.ts +94 -0
  33. package/src/client/lib/utils.ts +6 -0
  34. package/src/client/main.tsx +14 -0
  35. package/src/index.ts +19 -0
  36. package/src/node/cli.ts +54 -0
  37. package/src/node/config.ts +231 -0
  38. package/src/node/lines.ts +82 -0
  39. package/src/node/runtime.ts +102 -0
  40. package/src/node/server.ts +403 -0
  41. package/src/node/sources/command.ts +82 -0
  42. package/src/node/sources/file.ts +116 -0
  43. package/src/node/sources/ssh.ts +59 -0
  44. package/src/shared/columns.ts +41 -0
  45. package/src/shared/types.ts +188 -0
@@ -0,0 +1,43 @@
1
+ import { cn } from "@/lib/utils"
2
+
3
+ type ConnectionStatusProps = {
4
+ isConnected: boolean
5
+ isConnecting: boolean
6
+ className?: string
7
+ }
8
+
9
+ export function ConnectionStatus({
10
+ isConnected,
11
+ isConnecting,
12
+ className,
13
+ }: ConnectionStatusProps) {
14
+ const status = isConnected
15
+ ? {
16
+ dotClassName: "bg-emerald-500",
17
+ label: "Connected",
18
+ textClassName: "text-emerald-600 dark:text-emerald-400",
19
+ }
20
+ : isConnecting
21
+ ? {
22
+ dotClassName: "bg-amber-500",
23
+ label: "Connecting",
24
+ textClassName: "text-amber-600 dark:text-amber-400",
25
+ }
26
+ : {
27
+ dotClassName: "bg-muted-foreground/60",
28
+ label: "Disconnected",
29
+ textClassName: "text-muted-foreground",
30
+ }
31
+
32
+ return (
33
+ <div
34
+ className={cn("inline-flex items-center gap-2 text-xs font-medium", className)}
35
+ >
36
+ <span
37
+ aria-hidden="true"
38
+ className={cn("size-2 rounded-full", status.dotClassName)}
39
+ />
40
+ <span className={status.textClassName}>{status.label}</span>
41
+ </div>
42
+ )
43
+ }
@@ -0,0 +1,235 @@
1
+ /* eslint-disable react-refresh/only-export-components */
2
+ import { flexRender, type Cell } from "@tanstack/react-table"
3
+ import { Check, Minus } from "lucide-react"
4
+ import * as React from "react"
5
+
6
+ import type { DataTableColumnKind } from "./data-table-col-icon"
7
+ import type { DataTableColumnMeta } from "./data-table-header-col"
8
+ import { cn } from "@/lib/utils"
9
+
10
+ function getAlignmentClassName(align: DataTableColumnMeta["align"] = "left") {
11
+ switch (align) {
12
+ case "center":
13
+ return "justify-center text-center"
14
+ case "right":
15
+ return "justify-end text-right"
16
+ default:
17
+ return "justify-start text-left"
18
+ }
19
+ }
20
+
21
+ function formatDate(value: string | number | Date) {
22
+ const date = value instanceof Date ? value : new Date(value)
23
+
24
+ if (Number.isNaN(date.getTime())) {
25
+ return String(value)
26
+ }
27
+
28
+ return new Intl.DateTimeFormat(undefined, {
29
+ dateStyle: "medium",
30
+ }).format(date)
31
+ }
32
+
33
+ function formatNumber(value: number) {
34
+ return new Intl.NumberFormat(undefined, {
35
+ maximumFractionDigits: Number.isInteger(value) ? 0 : 2,
36
+ }).format(value)
37
+ }
38
+
39
+ function fallbackCellValue(value: unknown, kind?: DataTableColumnKind) {
40
+ if (value === null || value === undefined || value === "") {
41
+ return <span className="text-muted-foreground">-</span>
42
+ }
43
+
44
+ if (kind === "boolean" || typeof value === "boolean") {
45
+ return (
46
+ <span
47
+ className={cn(
48
+ "inline-flex min-w-16 items-center justify-center rounded-full border px-2 py-1 text-[11px] font-medium",
49
+ value
50
+ ? "border-border bg-accent text-accent-foreground"
51
+ : "border-border bg-muted text-muted-foreground"
52
+ )}
53
+ >
54
+ {value ? "True" : "False"}
55
+ </span>
56
+ )
57
+ }
58
+
59
+ if (kind === "number" || typeof value === "number") {
60
+ return formatNumber(Number(value))
61
+ }
62
+
63
+ if (kind === "date" || value instanceof Date) {
64
+ return formatDate(value as string | number | Date)
65
+ }
66
+
67
+ if (Array.isArray(value) || typeof value === "object") {
68
+ return (
69
+ <code className="rounded bg-muted px-1.5 py-1 font-mono text-[11px] text-muted-foreground">
70
+ {JSON.stringify(value)}
71
+ </code>
72
+ )
73
+ }
74
+
75
+ return String(value)
76
+ }
77
+
78
+ function renderHighlightedTextParts(value: string, terms: string[]) {
79
+ const normalizedTerms = Array.from(
80
+ new Set(terms.map((term) => term.trim()).filter(Boolean))
81
+ ).sort((left, right) => right.length - left.length)
82
+
83
+ if (normalizedTerms.length === 0) {
84
+ return [value]
85
+ }
86
+
87
+ const pattern = new RegExp(
88
+ `(${normalizedTerms
89
+ .map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
90
+ .join("|")})`,
91
+ "gi"
92
+ )
93
+ const parts = value.split(pattern)
94
+
95
+ return parts.map((part, index) => {
96
+ const matched = normalizedTerms.some(
97
+ (term) => part.toLowerCase() === term.toLowerCase()
98
+ )
99
+
100
+ if (!matched) {
101
+ return <React.Fragment key={`${part}-${index}`}>{part}</React.Fragment>
102
+ }
103
+
104
+ return (
105
+ <mark
106
+ className="rounded-sm bg-primary/20 ring-2 ring-primary/20 dark:bg-primary/50 dark:text-white dark:ring-primary/80"
107
+ key={`${part}-${index}`}
108
+ >
109
+ {part}
110
+ </mark>
111
+ )
112
+ })
113
+ }
114
+
115
+ function renderHighlightedText(
116
+ value: string,
117
+ terms: string[],
118
+ rendered: React.ReactNode
119
+ ) {
120
+ const highlightedParts = renderHighlightedTextParts(value, terms)
121
+
122
+ if (React.isValidElement(rendered)) {
123
+ const element = rendered as React.ReactElement<{
124
+ children?: React.ReactNode
125
+ }>
126
+
127
+ if (typeof element.props.children === "string") {
128
+ return React.cloneElement(element, undefined, highlightedParts)
129
+ }
130
+ }
131
+
132
+ return <span className="whitespace-pre-wrap">{highlightedParts}</span>
133
+ }
134
+
135
+ export function DataTableCheckbox({
136
+ checked,
137
+ indeterminate,
138
+ onCheckedChange,
139
+ ariaLabel,
140
+ }: {
141
+ checked: boolean
142
+ indeterminate?: boolean
143
+ onCheckedChange: (checked: boolean) => void
144
+ ariaLabel: string
145
+ }) {
146
+ const ref = React.useRef<HTMLInputElement>(null)
147
+
148
+ React.useEffect(() => {
149
+ if (!ref.current) {
150
+ return
151
+ }
152
+
153
+ ref.current.indeterminate = Boolean(indeterminate) && !checked
154
+ }, [checked, indeterminate])
155
+
156
+ return (
157
+ <label className="inline-flex cursor-pointer items-center justify-center">
158
+ <input
159
+ ref={ref}
160
+ aria-label={ariaLabel}
161
+ checked={checked}
162
+ className="peer sr-only"
163
+ onChange={(event) => onCheckedChange(event.target.checked)}
164
+ type="checkbox"
165
+ />
166
+ <span
167
+ className={cn(
168
+ "flex size-5 items-center justify-center rounded-[6px] border border-border bg-background text-primary-foreground shadow-xs transition-colors peer-focus-visible:ring-2 peer-focus-visible:ring-ring/50",
169
+ (checked || indeterminate) && "border-primary bg-primary"
170
+ )}
171
+ >
172
+ {indeterminate && !checked ? <Minus className="size-3.5" /> : null}
173
+ {checked ? <Check className="size-3.5" /> : null}
174
+ </span>
175
+ </label>
176
+ )
177
+ }
178
+
179
+ export function DataTableBodyCell<TData>({
180
+ cell,
181
+ highlightTerms = [],
182
+ paddingLeft,
183
+ paddingRight,
184
+ }: {
185
+ cell: Cell<TData, unknown>
186
+ highlightTerms?: string[]
187
+ paddingLeft?: React.CSSProperties["paddingLeft"]
188
+ paddingRight?: React.CSSProperties["paddingRight"]
189
+ }) {
190
+ const meta = (cell.column.columnDef.meta ?? {}) as DataTableColumnMeta
191
+ const rawValue = cell.getValue()
192
+ const rendered = flexRender(cell.column.columnDef.cell, cell.getContext())
193
+ const isSticky = meta.sticky === "left"
194
+ const isSelectionCell = meta.kind === "selection"
195
+ const shouldTruncate = meta.truncate ?? true
196
+ const shouldHighlight =
197
+ meta.kind === "text" &&
198
+ meta.highlightMatches !== false &&
199
+ typeof rawValue === "string" &&
200
+ highlightTerms.length > 0
201
+ const content = shouldHighlight
202
+ ? renderHighlightedText(rawValue, highlightTerms, rendered)
203
+ : (rendered ?? fallbackCellValue(rawValue, meta.kind))
204
+
205
+ return (
206
+ <td
207
+ data-sticky-cell={isSticky ? "true" : "false"}
208
+ className={cn(
209
+ "flex shrink-0 border-b border-border px-2 py-1.5 align-middle text-sm text-foreground",
210
+ getAlignmentClassName(meta.align),
211
+ isSticky && "sticky left-0 z-10 border-r border-r-border bg-card",
212
+ meta.cellClassName
213
+ )}
214
+ style={{
215
+ paddingLeft,
216
+ paddingRight,
217
+ width: cell.column.getSize(),
218
+ }}
219
+ >
220
+ <div
221
+ className={cn(
222
+ isSelectionCell
223
+ ? "flex w-full items-center justify-center"
224
+ : shouldTruncate
225
+ ? "min-w-0 truncate"
226
+ : "w-full min-w-0 break-words whitespace-normal"
227
+ )}
228
+ >
229
+ {content}
230
+ </div>
231
+ </td>
232
+ )
233
+ }
234
+
235
+ export { fallbackCellValue }
@@ -0,0 +1,73 @@
1
+ /* eslint-disable react-refresh/only-export-components */
2
+ import {
3
+ Braces,
4
+ CalendarDays,
5
+ CheckSquare2,
6
+ Hash,
7
+ ListFilter,
8
+ Type,
9
+ } from "lucide-react"
10
+ import type { LucideProps } from "lucide-react"
11
+ import type { ComponentType } from "react"
12
+
13
+ export type DataTableColumnKind =
14
+ | "text"
15
+ | "enum"
16
+ | "number"
17
+ | "boolean"
18
+ | "date"
19
+ | "json"
20
+ | "selection"
21
+
22
+ const iconMap: Record<DataTableColumnKind, ComponentType<LucideProps>> = {
23
+ text: Type,
24
+ enum: ListFilter,
25
+ number: Hash,
26
+ boolean: CheckSquare2,
27
+ date: CalendarDays,
28
+ json: Braces,
29
+ selection: CheckSquare2,
30
+ }
31
+
32
+ export function DataTableColIcon({
33
+ kind,
34
+ ...props
35
+ }: { kind: DataTableColumnKind } & LucideProps) {
36
+ const Icon = iconMap[kind]
37
+
38
+ return <Icon {...props} />
39
+ }
40
+
41
+ function isDateString(value: string) {
42
+ if (!/\d{4}-\d{2}-\d{2}/.test(value)) {
43
+ return false
44
+ }
45
+
46
+ return !Number.isNaN(Date.parse(value))
47
+ }
48
+
49
+ export function inferDataTableColumnKind(values: unknown[]) {
50
+ const sample = values.find((value) => value !== null && value !== undefined)
51
+
52
+ if (sample instanceof Date) {
53
+ return "date" as const
54
+ }
55
+
56
+ if (typeof sample === "boolean") {
57
+ return "boolean" as const
58
+ }
59
+
60
+ if (typeof sample === "number") {
61
+ return "number" as const
62
+ }
63
+
64
+ if (typeof sample === "string" && isDateString(sample)) {
65
+ return "date" as const
66
+ }
67
+
68
+ if (sample && typeof sample === "object") {
69
+ return "json" as const
70
+ }
71
+
72
+ return "text" as const
73
+ }
@@ -0,0 +1,225 @@
1
+ import { flexRender, type Header } from "@tanstack/react-table"
2
+ import { ArrowDown, ArrowUp, Search } from "lucide-react"
3
+ import * as React from "react"
4
+ import { createPortal } from "react-dom"
5
+
6
+ import {
7
+ DataTableColIcon,
8
+ type DataTableColumnKind,
9
+ } from "./data-table-col-icon"
10
+ import { cn } from "@/lib/utils"
11
+
12
+ export type DataTableAlign = "left" | "center" | "right"
13
+
14
+ export type DataTableColumnMeta = {
15
+ align?: DataTableAlign
16
+ cellClassName?: string
17
+ headerClassName?: string
18
+ highlightMatches?: boolean
19
+ kind?: DataTableColumnKind
20
+ sticky?: "left"
21
+ truncate?: boolean
22
+ }
23
+
24
+ function getAlignmentClassName(align: DataTableAlign = "left") {
25
+ switch (align) {
26
+ case "center":
27
+ return "justify-center text-center"
28
+ case "right":
29
+ return "justify-end text-right"
30
+ default:
31
+ return "justify-start text-left"
32
+ }
33
+ }
34
+
35
+ function HeaderSortIcon({ sorted }: { sorted: false | "asc" | "desc" }) {
36
+ if (sorted === "asc") {
37
+ return <ArrowUp className="size-3.5 text-foreground" />
38
+ }
39
+
40
+ if (sorted === "desc") {
41
+ return <ArrowDown className="size-3.5 text-foreground" />
42
+ }
43
+
44
+ return null
45
+ }
46
+
47
+ export function DataTableHeaderCol<TData>({
48
+ header,
49
+ highlightEnabled = false,
50
+ onToggleHighlight,
51
+ paddingLeft,
52
+ paddingRight,
53
+ scrollContainerRef,
54
+ }: {
55
+ header: Header<TData, unknown>
56
+ highlightEnabled?: boolean
57
+ onToggleHighlight?: () => void
58
+ paddingLeft?: React.CSSProperties["paddingLeft"]
59
+ paddingRight?: React.CSSProperties["paddingRight"]
60
+ scrollContainerRef: React.RefObject<HTMLDivElement | null>
61
+ }) {
62
+ const meta = (header.column.columnDef.meta ?? {}) as DataTableColumnMeta
63
+ const sorted = header.column.getIsSorted()
64
+ const canSort = header.column.getCanSort()
65
+ const alignmentClassName = getAlignmentClassName(meta.align)
66
+ const isSticky = meta.sticky === "left"
67
+
68
+ return (
69
+ <th
70
+ className={cn(
71
+ "relative flex shrink-0 border-b border-gray-300 bg-background py-2 align-middle text-xs font-medium tracking-wide text-muted-foreground uppercase dark:border-border",
72
+ isSticky && "sticky left-0 z-20 border-r border-r-border bg-background",
73
+ meta.headerClassName
74
+ )}
75
+ style={{
76
+ width: header.getSize(),
77
+ }}
78
+ >
79
+ {header.isPlaceholder ? null : canSort ? (
80
+ <div className="flex w-full min-w-0 items-center gap-2">
81
+ <button
82
+ className={cn(
83
+ "flex min-w-0 flex-1 items-center gap-1.5 rounded-md px-2 py-1 transition-colors hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none",
84
+ alignmentClassName
85
+ )}
86
+ onClick={header.column.getToggleSortingHandler()}
87
+ style={{
88
+ paddingLeft,
89
+ paddingRight,
90
+ }}
91
+ type="button"
92
+ >
93
+ {meta.kind && meta.kind !== "selection" ? (
94
+ <DataTableColIcon
95
+ className="size-3.5 shrink-0 text-muted-foreground"
96
+ kind={meta.kind}
97
+ />
98
+ ) : null}
99
+ <span className="truncate font-medium text-foreground normal-case">
100
+ {flexRender(header.column.columnDef.header, header.getContext())}
101
+ </span>
102
+ <HeaderSortIcon sorted={sorted} />
103
+ </button>
104
+ </div>
105
+ ) : (
106
+ <div
107
+ className={cn(
108
+ "flex w-full min-w-0 items-center gap-1 px-0.5 py-1",
109
+ alignmentClassName
110
+ )}
111
+ style={{
112
+ paddingLeft,
113
+ paddingRight,
114
+ }}
115
+ >
116
+ {meta.kind && meta.kind !== "selection" ? (
117
+ <DataTableColIcon
118
+ className="size-3.5 shrink-0 text-muted-foreground"
119
+ kind={meta.kind}
120
+ />
121
+ ) : null}
122
+ <span className="truncate font-medium text-foreground normal-case">
123
+ {flexRender(header.column.columnDef.header, header.getContext())}
124
+ </span>
125
+ {meta.kind === "text" && onToggleHighlight ? (
126
+ <button
127
+ className={cn(
128
+ "inline-flex size-5 items-center justify-center rounded-sm border",
129
+ highlightEnabled
130
+ ? "border-border bg-accent text-accent-foreground"
131
+ : "border-transparent text-muted-foreground"
132
+ )}
133
+ onClick={onToggleHighlight}
134
+ type="button"
135
+ >
136
+ <Search className="size-3" />
137
+ </button>
138
+ ) : null}
139
+ </div>
140
+ )}
141
+ {header.column.getCanResize() ? (
142
+ <ResizeHandler
143
+ header={header}
144
+ scrollContainerRef={scrollContainerRef}
145
+ />
146
+ ) : null}
147
+ </th>
148
+ )
149
+ }
150
+
151
+ function ResizeHandler<TData>({
152
+ header,
153
+ scrollContainerRef,
154
+ }: {
155
+ header: Header<TData, unknown>
156
+ scrollContainerRef: React.RefObject<HTMLDivElement | null>
157
+ }) {
158
+ const isResizing = header.column.getIsResizing()
159
+ const handleRef = React.useRef<HTMLDivElement>(null)
160
+ const [overlayBounds, setOverlayBounds] = React.useState<{
161
+ bottom: number
162
+ left: number
163
+ top: number
164
+ } | null>(null)
165
+ const transform = isResizing
166
+ ? `translateX(${
167
+ header.getContext().table.getState().columnSizingInfo.deltaOffset ?? 0
168
+ }px)`
169
+ : ""
170
+
171
+ React.useLayoutEffect(() => {
172
+ if (!isResizing || !handleRef.current || !scrollContainerRef.current) {
173
+ setOverlayBounds(null)
174
+ return
175
+ }
176
+
177
+ const updateBounds = () => {
178
+ if (!handleRef.current || !scrollContainerRef.current) {
179
+ return
180
+ }
181
+
182
+ const handleBounds = handleRef.current.getBoundingClientRect()
183
+ const containerBounds = scrollContainerRef.current.getBoundingClientRect()
184
+
185
+ setOverlayBounds({
186
+ bottom: Math.max(window.innerHeight - containerBounds.bottom, 0),
187
+ left: handleBounds.left,
188
+ top: containerBounds.top,
189
+ })
190
+ }
191
+
192
+ updateBounds()
193
+ window.addEventListener("resize", updateBounds)
194
+
195
+ return () => window.removeEventListener("resize", updateBounds)
196
+ }, [isResizing, scrollContainerRef])
197
+
198
+ return (
199
+ <>
200
+ {isResizing && overlayBounds
201
+ ? createPortal(
202
+ <div
203
+ className="pointer-events-none fixed z-[100] w-1 animate-in cursor-col-resize bg-border transition-colors duration-75 zoom-in-50"
204
+ style={{
205
+ bottom: overlayBounds.bottom,
206
+ left: overlayBounds.left,
207
+ top: overlayBounds.top,
208
+ transform,
209
+ }}
210
+ />,
211
+ document.body
212
+ )
213
+ : null}
214
+ <div
215
+ ref={handleRef}
216
+ className={cn(
217
+ "absolute top-0 right-[-2px] z-[100] h-full w-1 cursor-col-resize touch-none rounded-full bg-border opacity-0 transition-opacity select-none hover:opacity-100",
218
+ isResizing && "opacity-100"
219
+ )}
220
+ onMouseDown={header.getResizeHandler()}
221
+ onTouchStart={header.getResizeHandler()}
222
+ />
223
+ </>
224
+ )
225
+ }