Skip to content

Commit

Permalink
feat (toolbar):Debug toolbar (#686)
Browse files Browse the repository at this point in the history
Checked out toolbar stuff into this new clean branch as many many messy
changes of mind.

---------

Co-authored-by: msfstef <[email protected]>
  • Loading branch information
paulharter and msfstef authored Apr 30, 2024
1 parent 244066a commit abebbaa
Show file tree
Hide file tree
Showing 33 changed files with 21,064 additions and 81 deletions.
6 changes: 6 additions & 0 deletions .changeset/spicy-beans-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@electric-sql/debug-toolbar": patch
"electric-sql": patch
---

Adding debug toolbar
63 changes: 63 additions & 0 deletions .github/workflows/toolbar_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Toolbar / Tests

on:
push:
branches:
- main
pull_request:
paths:
- 'pnpm-lock.yaml'
- 'components/toolbar/**'

defaults:
run:
working-directory: components/toolbar

jobs:
verify_formatting:
name: Check formatting & linting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 18
cache: pnpm
- run: make deps
- run: pnpm run check-styleguide
check_types:
name: Check types
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 18
cache: pnpm
- run: make deps
- run: pnpm run typecheck
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [16, 18, 20]
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: pnpm
- name: Install
run: make deps
- name: Run tests
run: make tests
1 change: 1 addition & 0 deletions clients/typescript/src/satellite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { ShapeSubscription } from './process'
import { DbSchema } from '../client/model/schema'
import { QualifiedTablename } from '../util'

export { MockRegistry } from './mock'
export { SatelliteProcess } from './process'
export { GlobalRegistry, globalRegistry } from './registry'
export type { ShapeSubscription } from './process'
Expand Down
11 changes: 9 additions & 2 deletions clients/typescript/src/satellite/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export class MockSatelliteProcess implements Satellite {
socketFactory: SocketFactory
opts: SatelliteOpts
token: string | undefined
connectivityState?: ConnectivityState

constructor(
dbName: DbName,
Expand All @@ -87,8 +88,9 @@ export class MockSatelliteProcess implements Satellite {
this.notifier = notifier
this.socketFactory = socketFactory
this.opts = opts
this.connectivityState = { status: 'disconnected' }
}
connectivityState?: ConnectivityState | undefined

subscribe(_shapeDefinitions: Shape[]): Promise<ShapeSubscription> {
return Promise.resolve({
synced: Promise.resolve(),
Expand Down Expand Up @@ -161,6 +163,11 @@ export class MockRegistry extends BaseRegistry {

const opts = { ...satelliteDefaults, ...overrides }

const satellites = this.satellites
if (satellites[dbName] !== undefined) {
return satellites[dbName]
}

const satellite = new MockSatelliteProcess(
dbName,
adapter,
Expand All @@ -169,8 +176,8 @@ export class MockRegistry extends BaseRegistry {
socketFactory,
opts
)
this.satellites[dbName] = satellite
await satellite.start()

return satellite
}
}
Expand Down
5 changes: 5 additions & 0 deletions components/toolbar/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dist
node_modules

# We're ignoring the lockfiles here because this example is meant to act like `npx create-electric-app` in that they use latest deps
pnpm-lock.yaml
8 changes: 8 additions & 0 deletions components/toolbar/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*.log
*.md
.DS_Store
dist/
node_modules/
test/generated/
.gitignore
.github/
4 changes: 4 additions & 0 deletions components/toolbar/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}
9 changes: 9 additions & 0 deletions components/toolbar/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
deps:
pnpm install --frozen-lockfile
make -C ../../clients/typescript build

build: deps
pnpm run build

tests:
CI=true pnpm run test
46 changes: 46 additions & 0 deletions components/toolbar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<a href="https://electric-sql.com">
<picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://raw.githubusercontent.com/electric-sql/meta/main/identity/ElectricSQL-logo-light-trans.svg"
/>
<source media="(prefers-color-scheme: light)"
srcset="https://raw.githubusercontent.com/electric-sql/meta/main/identity/ElectricSQL-logo-black.svg"
/>
<img alt="ElectricSQL logo"
src="https://raw.githubusercontent.com/electric-sql/meta/main/identity/ElectricSQL-logo-black.svg"
/>
</picture>
</a>

# ElectricSQL - Developer Toolbar

These are a collection of tools that can be used by developers to help them debug their ElectricSQL apps

## Adding this toolbar to your project

Add the toolbar to your project's `devDependencies` in `package.json`

```sh
"devDependencies": {
...
"@electric-sql/debug-toolbar": "latest",
...
}
```

In your code after calling `electrify`, if in debug mode, import and pass the electric client into `addToolbar`:

```typescript
const electric = await electrify(conn, schema, config)

if (config.debug) {
const { addToolbar } = await import('@electric-sql/debug-toolbar')
addToolbar(electric)
}
```

This will add the toolbar to the bottom of your window




42 changes: 42 additions & 0 deletions components/toolbar/builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { build } from 'esbuild'
import inlineImage from 'esbuild-plugin-inline-image'
import inlineImport from 'esbuild-plugin-inline-import'
import packageJson from './package.json' assert { type: 'json' }
const { dependencies } = packageJson

const entryFile = 'src/index.tsx'
const shared = {
bundle: true,
entryPoints: [entryFile],
// Treat all dependencies in package.json as externals to keep bundle size to a minimum
external: Object.keys(dependencies),
logLevel: 'info',
minify: true,
sourcemap: true,
target: ['esnext', 'node12.22.0'],
plugins: [
inlineImage(),
inlineImport({
filter: /index\.css$/,
transform: (content) => {
// Remove comments
content = content.replace(/\/\*[\s\S]*?\*\//g, '')
// Remove whitespace and newlines
content = content.replace(/\n/g, '').replace(/\s\s+/g, ' ')
return content
},
}),
],
}

build({
...shared,
format: 'esm',
outfile: './dist/index.esm.js',
})

build({
...shared,
format: 'cjs',
outfile: './dist/index.cjs.js',
})
46 changes: 46 additions & 0 deletions components/toolbar/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "@electric-sql/debug-toolbar",
"version": "0.1.0",
"type": "module",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"author": "ElectricSQL",
"license": "Apache-2.0",
"scripts": {
"build": "rm -rf ./dist && node builder.js && tsc -p tsconfig.build.json",
"check-styleguide": "prettier --check --loglevel warn .",
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"engines": {
"node": ">=16.11.0"
},
"dependencies": {
"codemirror": "^5.65.16",
"react": "^18.2.0",
"react-codemirror2": "^8.0.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/better-sqlite3": "7.6.3",
"@types/node": "^20.12.7",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"better-sqlite3": "^8.4.0",
"electric-sql": "workspace:*",
"esbuild": "^0.20.2",
"esbuild-plugin-inline-image": "^0.0.9",
"esbuild-plugin-inline-import": "^1.0.4",
"jsdom": "24.0.0",
"prettier": "3.2.5",
"typescript": "^5.4.5",
"vitest": "^1.5.0"
},
"peerDependencies": {
"electric-sql": "workspace:*"
},
"files": [
"dist"
]
}
6 changes: 6 additions & 0 deletions components/toolbar/src/SvgModule.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
declare module '*.svg' {
import React = require('react')
export const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>
const src: string
export default src
}
19 changes: 19 additions & 0 deletions components/toolbar/src/api/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Row, Statement, ConnectivityState } from 'electric-sql/util'

export type UnsubscribeFunction = () => void

export interface ToolbarInterface {
getSatelliteNames(): string[]
getSatelliteStatus(name: string): ConnectivityState | null
subscribeToSatelliteStatus(
name: string,
callback: (connectivityState: ConnectivityState) => void,
): UnsubscribeFunction

toggleSatelliteStatus(name: string): Promise<void>

getSatelliteShapeSubscriptions(name: string): string[]

resetDb(dbName: string): Promise<void>
queryDb(dbName: string, statement: Statement): Promise<Row[]>
}
79 changes: 79 additions & 0 deletions components/toolbar/src/api/toolbar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { ToolbarInterface, UnsubscribeFunction } from './interface'
import { Row, Statement, ConnectivityState } from 'electric-sql/util'
import { Registry, GlobalRegistry, Satellite } from 'electric-sql/satellite'
import { SubscriptionsManager } from 'electric-sql/satellite/shapes'

export class Toolbar implements ToolbarInterface {
constructor(private registry: Registry | GlobalRegistry) {}

private getSatellite(name: string): Satellite {
const sat = this.registry.satellites[name]
if (!sat) {
throw new Error(`Satellite for db ${name} not found.`)
}
return sat
}

getSatelliteNames(): string[] {
return Object.keys(this.registry.satellites)
}

getSatelliteStatus(name: string): ConnectivityState | null {
const sat = this.getSatellite(name)
return sat.connectivityState ?? null
}

subscribeToSatelliteStatus(
name: string,
callback: (connectivityState: ConnectivityState) => void,
): UnsubscribeFunction {
const sat = this.getSatellite(name)

// call once immediately if connectivity state available
if (sat.connectivityState) {
callback(sat.connectivityState)
}
// subscribe to subsequent changes
return sat.notifier.subscribeToConnectivityStateChanges((notification) =>
callback(notification.connectivityState),
)
}

toggleSatelliteStatus(name: string): Promise<void> {
const sat = this.getSatellite(name)
if (sat.connectivityState?.status === 'connected') {
sat.clientDisconnect()
return Promise.resolve()
}
return sat.connectWithBackoff()
}

getSatelliteShapeSubscriptions(name: string): string[] {
const sat = this.getSatellite(name)
//@ts-expect-error accessing private field
const manager = sat['subscriptions'] as SubscriptionsManager
const shapes = JSON.parse(manager.serialize()) as Record<string, any>
return Object.entries(shapes).flatMap((shapeKeyDef) =>
shapeKeyDef[1].map((x: any) =>
JSON.stringify({ id: shapeKeyDef[0], ...x.definition }, null, 2),
),
)
}

resetDb(dbName: string): Promise<void> {
const DBDeleteRequest = window.indexedDB.deleteDatabase(dbName)
DBDeleteRequest.onsuccess = () =>
console.log('Database deleted successfully')

// the IndexedDB cannot be deleted if the database connection is still open,
// so we need to reload the page to close any open connections.
// On reload, the database will be recreated.
window.location.reload()
return Promise.resolve()
}

queryDb(dbName: string, statement: Statement): Promise<Row[]> {
const sat = this.getSatellite(dbName)
return sat.adapter.query(statement)
}
}
1 change: 1 addition & 0 deletions components/toolbar/src/css.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module '*.css'
Loading

0 comments on commit abebbaa

Please sign in to comment.