Skip to content

Commit 66b7356

Browse files
authored
Chat share functionnality (#7)
* feat: enhance chat functionality with session management - Updated `useChat` hook to accept an optional `initialSession` parameter for initializing chat history. - Modified `ChatComponent` to handle read-only mode and pass the initial session to the chat hook. - Enhanced `useSessionManager` to skip loading the current session if chat history is already populated. - Added session sharing capabilities in the `SessionSidebar`, including loading and managing session shares. - Introduced new API endpoints for creating, retrieving, and deleting session shares in the backend. This update improves user experience by allowing session persistence and sharing, enhancing the overall chat functionality. * Add CI steps for plugin metadata extraction, packaging, and validation; update Go dependencies * fix: no package * fix: no cursor plan * fix: no cursor plan * fix: no custom expiration * fix: session read only * fix: session read only * fix: e2e tests * fix: npm run dev * feat: add Redis support for session sharing and health checks - Introduced Redis configuration and client creation in the plugin. - Implemented fallback logic for in-memory storage if Redis is unavailable. - Updated health check to include Redis connection status. - Added Redis service to docker-compose for local development. * go dep * feat: enhance session sharing with expiration options - Updated ShareDialog to handle expiration options for shares, including "Never" and specific durations in hours and days. - Modified sessionShareService to accept both hours and days for expiration, improving flexibility. - Adjusted tests to validate new expiration logic and ensure correct behavior for share creation. - Refactored session management to support immediate saving of chat history and session states. - Cleaned up unused import and export functionalities in the session sidebar component. * fix: resolve session sharing expiration handling - Fixed issues with session sharing expiration options not being applied correctly. - Ensured that both "Never" and specific duration settings are functioning as intended. - Updated related tests to cover edge cases for expiration logic. * fix: coverage target * test: improve session sharing tests for revoke functionality and error handling - Enhanced the test for revoking session shares by ensuring the "Create Another Share" button is visible after a share is created. - Added checks for the visibility of the "Existing Shares" label before locating the revoke button. - Updated error handling test to avoid strict mode violations by using the first() method for the error message element. * fix bugbot * fix bugbot * fix: e2e ? * fix: e2e ? * fix: lint * fix: lint * fix: lint * fix: e2e - claude code better? * fix: claude <3 * fix: claude <3
1 parent ad131c3 commit 66b7356

44 files changed

Lines changed: 4626 additions & 658 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,33 @@ jobs:
6060
with:
6161
version: latest
6262
args: test
63+
64+
- name: Get plugin metadata
65+
id: metadata
66+
run: |
67+
sudo apt-get update && sudo apt-get install -y jq
68+
69+
export GRAFANA_PLUGIN_ID=$(cat dist/plugin.json | jq -r .id)
70+
export GRAFANA_PLUGIN_VERSION=$(cat dist/plugin.json | jq -r .info.version)
71+
export GRAFANA_PLUGIN_TYPE=$(cat dist/plugin.json | jq -r .type)
72+
export GRAFANA_PLUGIN_ARTIFACT=${GRAFANA_PLUGIN_ID}-${GRAFANA_PLUGIN_VERSION}.zip
73+
74+
echo "plugin-id=${GRAFANA_PLUGIN_ID}" >> $GITHUB_OUTPUT
75+
echo "plugin-version=${GRAFANA_PLUGIN_VERSION}" >> $GITHUB_OUTPUT
76+
echo "plugin-type=${GRAFANA_PLUGIN_TYPE}" >> $GITHUB_OUTPUT
77+
echo "archive=${GRAFANA_PLUGIN_ARTIFACT}" >> $GITHUB_OUTPUT
78+
79+
- name: Package plugin
80+
id: package-plugin
81+
run: |
82+
mv dist $PLUGIN_ID
83+
zip $PLUGIN_ARCHIVE $PLUGIN_ID -r
84+
env:
85+
PLUGIN_ID: ${{ steps.metadata.outputs.plugin-id }}
86+
PLUGIN_ARCHIVE: ${{ steps.metadata.outputs.archive }}
87+
88+
- name: Validate plugin
89+
run: |
90+
npx -y @grafana/plugin-validator@latest -sourceCodeUri file://./ $PLUGIN_ARCHIVE
91+
env:
92+
PLUGIN_ARCHIVE: ${{ steps.metadata.outputs.archive }}

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,11 @@ ci/
5353
.env.local
5454
.env.development
5555
.env.production
56+
57+
58+
# Grafana plugin
59+
consensys-asko11y-app/
60+
consensys-asko11y-app.zip
61+
62+
# Cursor plan file
63+
.cursor/plans/

AGENTS.md

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,49 @@ pkg/
211211
**Business Logic** (`src/core/services/SessionService.ts`):
212212

213213
- Always validate org context
214-
- Implement 10s debounce for auto-save
214+
- Auto-save when streaming completes (immediate save, no debounce)
215215
- Maintain org isolation within user's storage (sessions organized by org, but private to each user)
216216

217+
### Session Sharing
218+
219+
**Backend Implementation** (`pkg/plugin/shares.go`, `pkg/plugin/shares_redis.go`):
220+
221+
- **Storage Options**: In-memory (default) or Redis (optional, for production)
222+
- **Share Store Interface**: `ShareStoreInterface` allows pluggable storage backends
223+
- **Rate Limiting**: 50 shares per hour per user (prevents abuse)
224+
- **Expiration Handling**: Supports expiration in days or hours (hours converted to days internally)
225+
- **Organization Isolation**: Shares are scoped to the organization where created
226+
- **Secure IDs**: Cryptographically secure share IDs (32-byte random tokens, base64 URL-safe encoded)
227+
228+
**API Endpoints** (`pkg/plugin/plugin.go`):
229+
230+
- `POST /api/sessions/share` - Create a share link
231+
- `GET /api/sessions/shared/:shareId` - Get shared session (read-only, org-scoped)
232+
- `DELETE /api/sessions/share/:shareId` - Revoke a share link
233+
- `GET /api/sessions/:sessionId/shares` - List all shares for a session
234+
235+
**Frontend Implementation** (`src/services/sessionShare.ts`, `src/components/Chat/components/ShareDialog/ShareDialog.tsx`):
236+
237+
- `SessionShareService` - Client service for share operations
238+
- `ShareDialog` - UI component for creating and managing shares
239+
- `SharedSession` page (`src/pages/SharedSession.tsx`) - Read-only view for shared sessions
240+
- Expiration options: 1 hour, 1 day, 7 days, 30 days, 90 days, or never
241+
- Import functionality: Users can import shared sessions into their account
242+
243+
**Redis Support** (Optional):
244+
245+
- Configure Redis via environment variables or plugin config
246+
- `RedisShareStore` implements `ShareStoreInterface` for persistent storage
247+
- Automatic TTL handling (Redis manages expiration)
248+
- Session index sets for efficient lookup of all shares for a session
249+
- See `pkg/plugin/shares_redis.go` for implementation details
250+
251+
**Adding Redis Support:**
252+
253+
1. Set Redis connection details in plugin configuration or environment variables
254+
2. Backend automatically detects Redis availability and uses it if configured
255+
3. Falls back to in-memory storage if Redis is unavailable
256+
217257
### Backend Development Workflow
218258

219259
1. Make Go code changes
@@ -352,6 +392,17 @@ docker compose exec grafana curl http://mcp-grafana:8000/mcp
352392
# Auto-cleanup: triggers at quota limit
353393
```
354394

395+
**Session sharing issues:**
396+
397+
```bash
398+
# Check share link is accessible
399+
# Verify share hasn't expired (check expiration date)
400+
# Check organization context (shares are org-scoped)
401+
# Verify rate limit (50 shares per hour per user)
402+
# Check Redis connection if using Redis backend
403+
docker compose logs -f grafana | grep -i "share\|redis"
404+
```
405+
355406
## Key Files Reference
356407

357408
### Entry Points
@@ -364,9 +415,15 @@ docker compose exec grafana curl http://mcp-grafana:8000/mcp
364415

365416
- `pkg/plugin/plugin.go:140-191` - RBAC read-only tool list (`isReadOnlyTool()`)
366417
- `pkg/plugin/plugin.go:192-233` - RBAC filtering (`filterToolsByRole()`, `canAccessTool()`)
418+
- `pkg/plugin/plugin.go:600-850` - Session sharing API endpoints
419+
- `pkg/plugin/shares.go` - In-memory share store implementation
420+
- `pkg/plugin/shares_redis.go` - Redis-backed share store implementation
367421
- `pkg/mcp/client.go:49-75` - Multi-tenant header injection
368422
- `src/core/services/SessionService.ts` - Session business logic
369423
- `src/core/repositories/GrafanaUserStorageRepository.ts` - Session persistence (uses Grafana UserStorage API - per-user storage with localStorage fallback, organized by organization)
424+
- `src/services/sessionShare.ts` - Session sharing client service
425+
- `src/components/Chat/components/ShareDialog/ShareDialog.tsx` - Share dialog UI component
426+
- `src/pages/SharedSession.tsx` - Shared session read-only view page
370427
- `src/services/backendMCPClient.ts` - MCP proxy client
371428

372429
### Configuration

README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ See [Grafana documentation on allow_embedding](https://grafana.com/docs/grafana/
132132
### 💾 Smart Session Management
133133

134134
**Never Lose Your Work:**
135-
- **Auto-Save**: All conversations saved every 2 seconds
135+
- **Auto-Save**: All conversations saved automatically when streaming completes
136136
- **Session History**: Browse, resume, and manage previous conversations
137137
- **Organization Scoping**: Sessions organized by Grafana organization within each user's storage (sessions are private to each user)
138138
- **Import/Export**: Backup sessions as JSON or share with team members
@@ -145,6 +145,24 @@ See [Grafana documentation on allow_embedding](https://grafana.com/docs/grafana/
145145
- Search through conversation history
146146
- Export important conversations for documentation
147147

148+
### 🔗 Session Sharing
149+
150+
**Share Conversations with Your Team:**
151+
- **Shareable Links**: Create secure, shareable links for any chat session
152+
- **Flexible Expiration**: Set expiration times (1 hour, 1 day, 7 days, 30 days, 90 days, or never)
153+
- **Read-Only Viewing**: Recipients can view shared sessions in read-only mode
154+
- **Import to Account**: Import shared sessions into your own account for continued conversation
155+
- **Revoke Access**: Revoke share links at any time
156+
- **Rate Limited**: 50 shares per hour per user to prevent abuse
157+
- **Organization Isolation**: Shares are scoped to the organization where they were created
158+
159+
**How It Works:**
160+
1. Click the share button on any session
161+
2. Choose an expiration time (or set to never expire)
162+
3. Copy the generated share link
163+
4. Share the link with team members
164+
5. Recipients can view the session or import it to continue the conversation
165+
148166
### ⚙️ Customizable Configuration
149167

150168
- **System Prompts**: Customize AI behavior (default, replace, or append mode)
@@ -497,6 +515,18 @@ For detailed troubleshooting, see the [Troubleshooting Guide](src/README.md#trou
497515
3. Ensure organization context is correct
498516
4. Review Grafana RBAC policies if using Enterprise
499517

518+
### Session Sharing Issues
519+
520+
**Issue**: Share link not working, expired, or access denied
521+
522+
**Solutions**:
523+
1. **Check Expiration**: Verify the share link hasn't expired (check expiration date)
524+
2. **Organization Context**: Ensure accessing from the same Grafana organization where created
525+
3. **Rate Limit**: Maximum 50 shares per hour per user - wait if limit reached
526+
4. **Share Revoked**: Creator may have revoked the share link
527+
5. **Backend Storage**: If using in-memory storage, shares are lost on Grafana restart (use Redis for persistence)
528+
6. Check Grafana logs: `docker compose logs -f grafana | grep -i share`
529+
500530
### Build or Development Issues
501531

502532
**Issue**: Plugin fails to build or run in development

docker-compose.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
services:
2+
redis:
3+
image: redis:7-alpine
4+
ports:
5+
- "127.0.0.1:6379:6379"
6+
command: redis-server --appendonly yes
7+
volumes:
8+
- redis-data:/data
9+
healthcheck:
10+
test: ["CMD", "redis-cli", "ping"]
11+
interval: 5s
12+
timeout: 3s
13+
retries: 5
14+
215
grafana:
316
extends:
417
file: .config/docker-compose-base.yaml
@@ -9,6 +22,10 @@ services:
922
GF_AUTH_ANONYMOUS_ENABLED: false
1023
GF_SECURITY_ALLOW_EMBEDDING: true
1124
LLM_API_KEY: ${LLM_API_KEY:-}
25+
GF_PLUGIN_ASKO11Y_REDIS: ${GF_PLUGIN_ASKO11Y_REDIS:-redis://redis:6379/0}
26+
depends_on:
27+
redis:
28+
condition: service_healthy
1229

1330
mcp-grafana:
1431
image: grafana/mcp-grafana
@@ -55,3 +72,6 @@ services:
5572
depends_on:
5673
- mcp-grafana
5774

75+
volumes:
76+
redis-data:
77+

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ go 1.25.5
55
require (
66
github.com/grafana/grafana-plugin-sdk-go v0.285.0
77
github.com/modelcontextprotocol/go-sdk v1.1.0
8+
github.com/redis/go-redis/v9 v9.17.2
9+
golang.org/x/time v0.14.0
810
)
911

1012
require (
@@ -15,6 +17,7 @@ require (
1517
github.com/cespare/xxhash/v2 v2.3.0 // indirect
1618
github.com/cheekybits/genny v1.0.0 // indirect
1719
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
20+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
1821
github.com/fatih/color v1.17.0 // indirect
1922
github.com/go-logr/logr v1.4.3 // indirect
2023
github.com/go-logr/stdr v1.2.2 // indirect

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc=
99
github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g=
1010
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
1111
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
12+
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
13+
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
14+
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
15+
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
1216
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
1317
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
1418
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
@@ -24,6 +28,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
2428
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2529
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
2630
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
31+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
32+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
2733
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
2834
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
2935
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
@@ -149,6 +155,8 @@ github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+L
149155
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
150156
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
151157
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
158+
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
159+
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
152160
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
153161
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
154162
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -269,6 +277,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
269277
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
270278
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
271279
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
280+
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
281+
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
272282
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
273283
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
274284
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=

package-lock.json

Lines changed: 32 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@
2828
"coverage": "npm run coverage:clean && npm run coverage:unit && npm run build:frontend:coverage && npm run e2e:coverage && npm run coverage:merge",
2929
"server": "docker compose up --build",
3030
"sign": "npx --yes @grafana/sign-plugin@latest",
31-
"backend:mod": "go mod tidy"
31+
"backend:mod": "go mod tidy",
32+
"validate": "npm run validate:build && npm run validate:archive && npm run validate:run",
33+
"validate:build": "npm run build:prod",
34+
"validate:archive": "PLUGIN_ID=$(node -e \"console.log(require('./src/plugin.json').id)\") && rm -rf \"${PLUGIN_ID}\" \"${PLUGIN_ID}.zip\" && mkdir -p \"${PLUGIN_ID}\" && cp -r dist/* \"${PLUGIN_ID}/\" && zip -qr \"${PLUGIN_ID}.zip\" \"${PLUGIN_ID}\" && echo \"Created ${PLUGIN_ID}.zip\"",
35+
"validate:run": "PLUGIN_ID=$(node -e \"console.log(require('./src/plugin.json').id)\") && npx -y @grafana/plugin-validator@latest -sourceCodeUri file://. \"${PLUGIN_ID}.zip\"",
36+
"validate:clean": "PLUGIN_ID=$(node -e \"console.log(require('./src/plugin.json').id)\") && rm -rf \"${PLUGIN_ID}\" \"${PLUGIN_ID}.zip\""
3237
},
3338
"author": "Consensys",
3439
"license": "MIT",
@@ -38,7 +43,7 @@
3843
"@babel/preset-react": "^7.28.5",
3944
"@babel/preset-typescript": "^7.28.5",
4045
"@grafana/eslint-config": "^8.2.0",
41-
"@grafana/plugin-e2e": "^2.2.0",
46+
"@grafana/plugin-e2e": "^3.1.4",
4247
"@grafana/tsconfig": "^2.0.0",
4348
"@istanbuljs/nyc-config-typescript": "^1.0.2",
4449
"@jest/test-sequencer": "^29.7.0",

0 commit comments

Comments
 (0)