Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 161 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,75 +1,175 @@
# React + TypeScript + Vite
# Hive Labaratory Component Documentation

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
`Labaratory` is a fully featured React component that provides a modern GraphQL playground experience (collections, tabs, history, preflight scripts, environment variables, tests, etc.). It ships with an opinionated UI and a context provider that exposes granular hooks for integrating with your own storage or analytics layers.

Currently, two official plugins are available:
This document explains how to embed the component, what data it needs, and how to react to user changes through the available callbacks.

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
---

## React Compiler
## Getting Started

The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information.
```tsx
import { Labaratory } from "@/components/labaratory/labaratory";

Note: This will impact Vite dev & build performances.
export const Playground = () => {
return (
<Labaratory
defaultEndpoint="https://api.spacex.land/graphql/"
onEndpointChange={(endpoint) => {
localStorage.setItem("lab-endpoint", endpoint ?? "");
}}
defaultOperations={[]}
onOperationCreate={(operation) => console.log("created", operation)}
onOperationUpdate={(operation) => console.log("updated", operation)}
onOperationDelete={(operation) => console.log("deleted", operation)}
/>
);
};
```

The component renders the full UI and injects a `LabaratoryProvider`, so any nested component can call `useLabaratory()` to access the current state.

---

## Data Flow Overview

`Labaratory` is controlled via two complementary mechanisms:

1. **Default Values** – `default*` props let you hydrate the playground from persisted data (e.g., localStorage, database). These are only read during initialization.
2. **Event Callbacks** – `on*` props fire whenever users create/update/delete entities within the playground. Use them to keep external storage in sync or to trigger side effects (analytics, notifications, etc.).

If you provide both a default value and a callback for the same entity, you can make the playground fully persistent without touching its internals.

---

## Props Reference

### Endpoint

- `defaultEndpoint?: string | null`
- `onEndpointChange?: (endpoint: string | null) => void`

### Collections

- `defaultCollections?: LabaratoryCollection[]`
- `onCollectionsChange?: (collections: LabaratoryCollection[]) => void`
- `onCollectionCreate?: (collection: LabaratoryCollection) => void`
- `onCollectionUpdate?: (collection: LabaratoryCollection) => void`
- `onCollectionDelete?: (collection: LabaratoryCollection) => void`
- `onCollectionOperationCreate?: (collection: LabaratoryCollection, operation: LabaratoryCollectionOperation) => void`
- `onCollectionOperationUpdate?: (collection: LabaratoryCollection, operation: LabaratoryCollectionOperation) => void`
- `onCollectionOperationDelete?: (collection: LabaratoryCollection, operation: LabaratoryCollectionOperation) => void`

### Operations

- `defaultOperations?: LabaratoryOperation[]`
- `defaultActiveOperationId?: string`
- `onOperationsChange?: (operations: LabaratoryOperation[]) => void`
- `onActiveOperationIdChange?: (operationId: string) => void`
- `onOperationCreate?: (operation: LabaratoryOperation) => void`
- `onOperationUpdate?: (operation: LabaratoryOperation) => void`
- `onOperationDelete?: (operation: LabaratoryOperation) => void`

### History

- `defaultHistory?: LabaratoryHistory[]`
- `onHistoryChange?: (history: LabaratoryHistory[]) => void`
- `onHistoryCreate?: (history: LabaratoryHistory) => void`
- `onHistoryUpdate?: (history: LabaratoryHistory) => void`
- `onHistoryDelete?: (history: LabaratoryHistory) => void`

### Tabs

- `defaultTabs?: LabaratoryTab[]`
- `defaultActiveTabId?: string | null`
- `onTabsChange?: (tabs: LabaratoryTab[]) => void`
- `onActiveTabIdChange?: (tabId: string | null) => void`

## Expanding the ESLint configuration
### Preflight Script

If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
- `defaultPreflight?: LabaratoryPreflight | null`
- `onPreflightChange?: (preflight: LabaratoryPreflight | null) => void`

```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
### Environment Variables

// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
- `defaultEnv?: LabaratoryEnv | null`
- `onEnvChange?: (env: LabaratoryEnv | null) => void`

// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
### Settings

- `defaultSettings?: LabaratorySettings | null`
- `onSettingsChange?: (settings: LabaratorySettings | null) => void`

### Tests

- `defaultTests?: LabaratoryTest[]`
- `onTestsChange?: (tests: LabaratoryTest[]) => void`

### Dialog Helpers

`useLabaratory()` also exposes `openAddCollectionDialog`, `openUpdateEndpointDialog`, and `openAddTestDialog` so that external buttons can toggle the built-in dialogs.

---

## Consuming State via `useLabaratory`

Inside any descendant of `Labaratory`, call the hook to access live state and actions:

```tsx
import { useLabaratory } from "@/components/labaratory/context";

const RunButton = () => {
const { runActiveOperation, endpoint } = useLabaratory();

return (
<button
disabled={!endpoint}
onClick={() => runActiveOperation(endpoint!, { env: { variables: {} } })}
>
Run
</button>
);
};
```

You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:

```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'

export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
All actions returned by the hook (collections, operations, history, tabs, preflight, env, settings, tests) stay in sync with the UI.

---

## Persistence Example

The snippet below demonstrates how to persist operations and history to `localStorage` using the granular callbacks:

```tsx
const STORAGE_KEY = "labaratory-data";

const loadData = () => {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}");
} catch {
return {};
}
};

const save = (partial: any) => {
const current = loadData();
localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...current, ...partial }));
};

const data = loadData();

export function PersistentPlayground() {
return (
<Labaratory
defaultOperations={data.operations ?? []}
onOperationsChange={(operations) => save({ operations })}
onOperationCreate={(operation) =>
console.info("operation created", operation)
}
defaultHistory={data.history ?? []}
onHistoryDelete={(history) => console.info("history deleted", history)}
onHistoryChange={(history) => save({ history })}
/>
);
}
```
33 changes: 0 additions & 33 deletions electron/main.cjs

This file was deleted.

5 changes: 0 additions & 5 deletions electron/preload.cjs

This file was deleted.

2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<title>Hive Laboratory</title>

<link href="/src/index.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/react-scan/dist/auto.global.js"></script>
<!-- <script src="https://cdn.jsdelivr.net/npm/react-scan/dist/auto.global.js"></script> -->

</head>
<body class="dark w-full h-full">
Expand Down
16 changes: 11 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"private": true,
"version": "0.0.0",
"type": "module",
"main": "electron/main.cjs",
"build": {
"appId": "com.guild.hive.laboratory",
"productName": "Hive Laboratory",
Expand All @@ -20,15 +19,16 @@
"dev": "vite",
"dev:electron": "VITE_TARGET=electron concurrently \"vite\" \"wait-on http://localhost:5173 && electron .\"",
"build": "tsc -b && vite build",
"dist:electron": "VITE_TARGET=electron vite build && electron-builder",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@dagrejs/dagre": "^1.1.8",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@mlc-ai/web-llm": "^0.2.79",
"@monaco-editor/react": "4.8.0-rc.2",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
Expand All @@ -48,18 +48,21 @@
"@tanstack/react-form": "^1.23.8",
"@tanstack/react-router": "^1.135.0",
"@tanstack/react-router-devtools": "^1.135.0",
"@xyflow/react": "^12.9.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"crypto-js": "^4.2.0",
"date-fns": "^4.1.0",
"esbuild": "^0.25.11",
"graphql": "^16.11.0",
"graphql-ws": "^6.0.6",
"lodash": "^4.17.21",
"lucide-react": "^0.548.0",
"lz-string": "^1.5.0",
"monaco-editor": "^0.54.0",
"monaco-editor": "^0.52.2",
"monaco-graphql": "^1.7.2",
"monacopilot": "^1.2.7",
"next-themes": "^0.4.6",
"react": "^19.1.1",
"react-dom": "^19.1.1",
Expand All @@ -73,15 +76,14 @@
"devDependencies": {
"@eslint/js": "^9.36.0",
"@tanstack/router-plugin": "^1.135.0",
"@types/crypto-js": "^4.2.2",
"@types/lodash": "^4.17.20",
"@types/node": "^24.6.0",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.4",
"babel-plugin-react-compiler": "^19.1.0-rc.3",
"concurrently": "^9.2.1",
"electron": "^39.1.1",
"electron-builder": "^26.0.12",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22",
Expand All @@ -96,5 +98,9 @@
"overrides": {
"vite": "npm:[email protected]"
}
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
Loading