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,37 @@
1
+ import {
2
+ Ban,
3
+ Check,
4
+ CircleAlert,
5
+ Copy,
6
+ Download,
7
+ ExternalLink,
8
+ Filter,
9
+ Info,
10
+ Play,
11
+ RefreshCcw,
12
+ Search,
13
+ Trash2,
14
+ X,
15
+ } from "lucide-react"
16
+ import type { ComponentType } from "react"
17
+
18
+ import type { DatoolIconName } from "../../shared/types"
19
+
20
+ export const LOG_VIEWER_ICONS: Record<
21
+ DatoolIconName,
22
+ ComponentType<{ className?: string }>
23
+ > = {
24
+ Ban,
25
+ Check,
26
+ CircleAlert,
27
+ Copy,
28
+ Download,
29
+ ExternalLink,
30
+ Filter,
31
+ Info,
32
+ Play,
33
+ RefreshCcw,
34
+ Search,
35
+ Trash: Trash2,
36
+ X,
37
+ }
@@ -0,0 +1,159 @@
1
+ import type { VisibilityState } from "@tanstack/react-table"
2
+
3
+ type PersistedTableState = {
4
+ columnFilters?: unknown[]
5
+ columnSizing?: Record<string, number>
6
+ columnVisibility?: VisibilityState
7
+ globalFilter?: string
8
+ highlightedColumns?: Record<string, boolean>
9
+ sorting?: unknown[]
10
+ }
11
+
12
+ const STREAM_PARAM = "stream"
13
+ const DATA_TABLE_URL_PARAM_PREFIX = "datatable-"
14
+ const LOG_VIEWER_TABLE_ID_PREFIX = "datool"
15
+
16
+ function getSearchUrlParam(tableId: string) {
17
+ return `${tableId}-search`
18
+ }
19
+
20
+ function getTableUrlParam(tableId: string) {
21
+ return `${DATA_TABLE_URL_PARAM_PREFIX}${tableId}`
22
+ }
23
+
24
+ function isDatoolUrlParam(key: string) {
25
+ return (
26
+ key.startsWith(`${DATA_TABLE_URL_PARAM_PREFIX}${LOG_VIEWER_TABLE_ID_PREFIX}`) ||
27
+ (key.startsWith(`${LOG_VIEWER_TABLE_ID_PREFIX}-`) && key.endsWith("-search"))
28
+ )
29
+ }
30
+
31
+ function readPersistedTableState(tableId: string) {
32
+ if (typeof window === "undefined") {
33
+ return null
34
+ }
35
+
36
+ try {
37
+ const rawValue = new URL(window.location.href).searchParams.get(
38
+ getTableUrlParam(tableId)
39
+ )
40
+
41
+ return rawValue ? (JSON.parse(rawValue) as PersistedTableState) : null
42
+ } catch {
43
+ return null
44
+ }
45
+ }
46
+
47
+ function sanitizeColumnVisibility(
48
+ columnVisibility: VisibilityState | undefined,
49
+ columnIds: string[]
50
+ ) {
51
+ const validIds = new Set(columnIds)
52
+
53
+ return Object.fromEntries(
54
+ Object.entries(columnVisibility ?? {}).filter(([columnId]) =>
55
+ validIds.has(columnId)
56
+ )
57
+ )
58
+ }
59
+
60
+ function cleanUpDatoolParams(url: URL, tableId: string) {
61
+ const activeSearchParam = getSearchUrlParam(tableId)
62
+ const activeTableParam = getTableUrlParam(tableId)
63
+
64
+ for (const key of Array.from(url.searchParams.keys())) {
65
+ if (
66
+ key === STREAM_PARAM ||
67
+ key === activeSearchParam ||
68
+ key === activeTableParam
69
+ ) {
70
+ continue
71
+ }
72
+
73
+ if (isDatoolUrlParam(key)) {
74
+ url.searchParams.delete(key)
75
+ }
76
+ }
77
+ }
78
+
79
+ export function readSelectedStreamId() {
80
+ if (typeof window === "undefined") {
81
+ return null
82
+ }
83
+
84
+ return new URL(window.location.href).searchParams.get(STREAM_PARAM)
85
+ }
86
+
87
+ export function readDatoolSearch(tableId: string) {
88
+ if (typeof window === "undefined") {
89
+ return ""
90
+ }
91
+
92
+ return new URL(window.location.href).searchParams.get(getSearchUrlParam(tableId)) ?? ""
93
+ }
94
+
95
+ export function readDatoolColumnVisibility(
96
+ tableId: string,
97
+ columnIds: string[]
98
+ ) {
99
+ return sanitizeColumnVisibility(
100
+ readPersistedTableState(tableId)?.columnVisibility,
101
+ columnIds
102
+ )
103
+ }
104
+
105
+ export function writeDatoolUrlState({
106
+ columnIds,
107
+ columnVisibility,
108
+ search,
109
+ selectedStreamId,
110
+ tableId,
111
+ }: {
112
+ columnIds: string[]
113
+ columnVisibility: VisibilityState
114
+ search: string
115
+ selectedStreamId: string | null
116
+ tableId: string
117
+ }) {
118
+ if (typeof window === "undefined") {
119
+ return
120
+ }
121
+
122
+ const url = new URL(window.location.href)
123
+ const searchValue = search.trim()
124
+ const nextColumnVisibility = sanitizeColumnVisibility(
125
+ columnVisibility,
126
+ columnIds
127
+ )
128
+ const nextTableState = {
129
+ ...readPersistedTableState(tableId),
130
+ } satisfies PersistedTableState
131
+
132
+ cleanUpDatoolParams(url, tableId)
133
+
134
+ if (selectedStreamId) {
135
+ url.searchParams.set(STREAM_PARAM, selectedStreamId)
136
+ } else {
137
+ url.searchParams.delete(STREAM_PARAM)
138
+ }
139
+
140
+ if (searchValue) {
141
+ url.searchParams.set(getSearchUrlParam(tableId), search)
142
+ } else {
143
+ url.searchParams.delete(getSearchUrlParam(tableId))
144
+ }
145
+
146
+ if (Object.keys(nextColumnVisibility).length > 0) {
147
+ nextTableState.columnVisibility = nextColumnVisibility
148
+ } else {
149
+ delete nextTableState.columnVisibility
150
+ }
151
+
152
+ if (Object.keys(nextTableState).length > 0) {
153
+ url.searchParams.set(getTableUrlParam(tableId), JSON.stringify(nextTableState))
154
+ } else {
155
+ url.searchParams.delete(getTableUrlParam(tableId))
156
+ }
157
+
158
+ window.history.replaceState(window.history.state, "", url)
159
+ }
@@ -0,0 +1,146 @@
1
+ import type { FilterFn } from "@tanstack/react-table"
2
+
3
+ import { type DataTableColumnConfig } from "@/components/data-table"
4
+ import {
5
+ buildEnumOptions,
6
+ matchesFieldClauses,
7
+ type DataTableSearchField,
8
+ type DataTableSearchFieldKind,
9
+ type DataTableSearchFilterClause,
10
+ } from "@/lib/data-table-search"
11
+
12
+ type TableRow = Record<string, unknown>
13
+
14
+ export type BuildTableSearchFieldsOptions = {
15
+ fieldOptions?: Partial<Record<string, string[]>>
16
+ }
17
+
18
+ function stringifySampleValue(value: unknown) {
19
+ if (value === null || value === undefined) {
20
+ return new Date().toISOString()
21
+ }
22
+
23
+ if (value instanceof Date) {
24
+ return value.toISOString()
25
+ }
26
+
27
+ if (typeof value === "object") {
28
+ return JSON.stringify(value)
29
+ }
30
+
31
+ return String(value)
32
+ }
33
+
34
+ function getColumnValueGetter<TData extends TableRow>(
35
+ column: DataTableColumnConfig<TData>
36
+ ) {
37
+ return (row: TData) => {
38
+ if (column.accessorFn) {
39
+ return column.accessorFn(row)
40
+ }
41
+
42
+ if (column.accessorKey) {
43
+ return row[column.accessorKey]
44
+ }
45
+
46
+ return undefined
47
+ }
48
+ }
49
+
50
+ function getDefaultFieldKind<TData extends TableRow>(
51
+ column: DataTableColumnConfig<TData>
52
+ ): DataTableSearchFieldKind {
53
+ switch (column.kind) {
54
+ case "date":
55
+ return "date"
56
+ case "enum":
57
+ return "enum"
58
+ case "json":
59
+ return "json"
60
+ case "number":
61
+ return "number"
62
+ default:
63
+ return "text"
64
+ }
65
+ }
66
+
67
+ export function resolveTableColumnId<TData extends TableRow>(
68
+ column: DataTableColumnConfig<TData>,
69
+ index: number
70
+ ) {
71
+ return (
72
+ column.id ??
73
+ column.accessorKey ??
74
+ (column.header
75
+ ? column.header.toLowerCase().replace(/\s+/g, "-")
76
+ : `column-${index}`)
77
+ )
78
+ }
79
+
80
+ export function buildTableSearchFields<TData extends TableRow>(
81
+ columns: DataTableColumnConfig<TData>[],
82
+ rows: TData[],
83
+ options: BuildTableSearchFieldsOptions = {}
84
+ ) {
85
+ const latestRow = rows.at(-1)
86
+
87
+ return columns.map<DataTableSearchField<TData>>((column, index) => {
88
+ const columnId = resolveTableColumnId(column, index)
89
+ const getValue = getColumnValueGetter(column)
90
+ const kind = getDefaultFieldKind(column)
91
+
92
+ if (kind === "enum") {
93
+ return {
94
+ getValue,
95
+ id: columnId,
96
+ kind,
97
+ options:
98
+ options.fieldOptions?.[columnId] ?? buildEnumOptions(rows, getValue),
99
+ }
100
+ }
101
+
102
+ if (kind === "date") {
103
+ return {
104
+ getValue,
105
+ id: columnId,
106
+ kind,
107
+ sample: stringifySampleValue(
108
+ latestRow ? getValue(latestRow) : new Date().toISOString()
109
+ ),
110
+ }
111
+ }
112
+
113
+ return {
114
+ getValue,
115
+ id: columnId,
116
+ kind,
117
+ }
118
+ })
119
+ }
120
+
121
+ export function withColumnSearchFilters<TData extends TableRow>(
122
+ columns: DataTableColumnConfig<TData>[],
123
+ fields: DataTableSearchField<TData>[]
124
+ ) {
125
+ const fieldMap = new Map(fields.map((field) => [field.id, field]))
126
+
127
+ return columns.map((column, index) => {
128
+ const columnId = resolveTableColumnId(column, index)
129
+ const field = fieldMap.get(columnId)
130
+
131
+ if (!field || column.filterFn) {
132
+ return column
133
+ }
134
+
135
+ return {
136
+ ...column,
137
+ filterFn: ((row, _columnId, filterValue) => {
138
+ const clauses = Array.isArray(filterValue)
139
+ ? (filterValue as DataTableSearchFilterClause[])
140
+ : []
141
+
142
+ return matchesFieldClauses(row.original, field, clauses)
143
+ }) as FilterFn<TData>,
144
+ } satisfies DataTableColumnConfig<TData>
145
+ })
146
+ }
@@ -0,0 +1,94 @@
1
+ export type SearchStatePersistence = "localStorage" | "url"
2
+
3
+ function getSearchLocalStorageKey(tableId: string) {
4
+ return `${tableId}:search`
5
+ }
6
+
7
+ function getSearchUrlParam(tableId: string) {
8
+ return `${tableId}-search`
9
+ }
10
+
11
+ function getTableUrlParam(tableId: string) {
12
+ return `datatable-${tableId}`
13
+ }
14
+
15
+ export function hasSearchUrlPersistence(tableId: string) {
16
+ if (typeof window === "undefined") {
17
+ return false
18
+ }
19
+
20
+ const params = new URL(window.location.href).searchParams
21
+
22
+ return (
23
+ params.has(getSearchUrlParam(tableId)) ||
24
+ params.has(getTableUrlParam(tableId))
25
+ )
26
+ }
27
+
28
+ export function getInitialSearchPersistence(
29
+ tableId: string
30
+ ): SearchStatePersistence {
31
+ return hasSearchUrlPersistence(tableId) ? "url" : "localStorage"
32
+ }
33
+
34
+ export function readPersistedSearch(
35
+ tableId: string,
36
+ statePersistence: SearchStatePersistence
37
+ ) {
38
+ if (typeof window === "undefined") {
39
+ return ""
40
+ }
41
+
42
+ if (statePersistence === "url") {
43
+ return (
44
+ new URL(window.location.href).searchParams.get(
45
+ getSearchUrlParam(tableId)
46
+ ) ?? ""
47
+ )
48
+ }
49
+
50
+ return window.localStorage.getItem(getSearchLocalStorageKey(tableId)) ?? ""
51
+ }
52
+
53
+ export function writePersistedSearch(
54
+ tableId: string,
55
+ statePersistence: SearchStatePersistence,
56
+ value: string
57
+ ) {
58
+ if (typeof window === "undefined") {
59
+ return
60
+ }
61
+
62
+ if (statePersistence === "url") {
63
+ const url = new URL(window.location.href)
64
+
65
+ if (value.trim()) {
66
+ url.searchParams.set(getSearchUrlParam(tableId), value)
67
+ } else {
68
+ url.searchParams.delete(getSearchUrlParam(tableId))
69
+ }
70
+
71
+ window.history.replaceState(window.history.state, "", url)
72
+ return
73
+ }
74
+
75
+ if (value) {
76
+ window.localStorage.setItem(getSearchLocalStorageKey(tableId), value)
77
+ return
78
+ }
79
+
80
+ window.localStorage.removeItem(getSearchLocalStorageKey(tableId))
81
+ }
82
+
83
+ export function clearUrlSearchPersistence(tableId: string) {
84
+ if (typeof window === "undefined") {
85
+ return
86
+ }
87
+
88
+ const url = new URL(window.location.href)
89
+
90
+ url.searchParams.delete(getSearchUrlParam(tableId))
91
+ url.searchParams.delete(getTableUrlParam(tableId))
92
+
93
+ window.history.replaceState(window.history.state, "", url)
94
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,14 @@
1
+ import { StrictMode } from "react"
2
+ import { createRoot } from "react-dom/client"
3
+
4
+ import "./index.css"
5
+ import App from "./App.tsx"
6
+ import { ThemeProvider } from "./components/theme-provider.tsx"
7
+
8
+ createRoot(document.getElementById("root")!).render(
9
+ <StrictMode>
10
+ <ThemeProvider storageKey="datool-theme">
11
+ <App />
12
+ </ThemeProvider>
13
+ </StrictMode>
14
+ )
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { commandSource } from "./node/sources/command"
2
+ import { fileSource } from "./node/sources/file"
3
+ import { sshSource } from "./node/sources/ssh"
4
+ import type { DatoolConfig } from "./shared/types"
5
+
6
+ export * from "./shared/types"
7
+ export { startDatoolServer } from "./node/server"
8
+
9
+ export function defineDatoolConfig<TConfig extends DatoolConfig>(
10
+ config: TConfig
11
+ ) {
12
+ return config
13
+ }
14
+
15
+ export const sources = {
16
+ command: commandSource,
17
+ file: fileSource,
18
+ ssh: sshSource,
19
+ }
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bun
2
+ import { startDatoolServer } from "./server"
3
+
4
+ function parseCliArgs(argv: string[]) {
5
+ const parsedArgs: {
6
+ configPath?: string
7
+ host?: string
8
+ port?: number
9
+ } = {}
10
+
11
+ for (let index = 0; index < argv.length; index += 1) {
12
+ const currentArg = argv[index]
13
+ const nextArg = argv[index + 1]
14
+
15
+ if (currentArg === "--host" && nextArg) {
16
+ parsedArgs.host = nextArg
17
+ index += 1
18
+ continue
19
+ }
20
+
21
+ if (currentArg === "--port" && nextArg) {
22
+ parsedArgs.port = Number.parseInt(nextArg, 10)
23
+ index += 1
24
+ continue
25
+ }
26
+
27
+ if (currentArg === "--config" && nextArg) {
28
+ parsedArgs.configPath = nextArg
29
+ index += 1
30
+ }
31
+ }
32
+
33
+ return parsedArgs
34
+ }
35
+
36
+ async function main() {
37
+ const args = parseCliArgs(process.argv.slice(2))
38
+ const server = await startDatoolServer({
39
+ configPath: args.configPath,
40
+ host: args.host,
41
+ port: args.port,
42
+ })
43
+
44
+ console.log(`datool ready`)
45
+ console.log(`config: ${server.configPath}`)
46
+ console.log(`url: ${server.url}`)
47
+ }
48
+
49
+ main().catch((error) => {
50
+ const message = error instanceof Error ? error.message : String(error)
51
+
52
+ console.error(`Failed to start datool: ${message}`)
53
+ process.exit(1)
54
+ })