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
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

### Added

- **Saved Queries Feature**: Users can now save, edit, and delete queries directly from the UI

- New "Saved" dropdown button in the editor panel for quick access to saved queries
- Queries are stored in a SQLite database for persistence
- Support for organizing queries by category
- Queries can be marked as either "query" or "mutate" type
- Full CRUD operations via REST API (`/api/saved-queries`)

- **New Configuration Options**:

- `--queries-db` flag to specify the SQLite database path
- `RATEL_QUERIES_DB` environment variable as alternative configuration
- Default database location: `<user-config-dir>/ratel/queries.db` (falls back to temp dir)

- **Save Query Modal**: New modal dialog for saving queries with:
- Name (required)
- Description (optional)
- Category (defaults to "General")
- Action type (query or mutate)

### Changed

- Replaced YAML-based preloaded queries with SQLite-based saved queries
- Queries are now user-editable through the UI instead of requiring file changes

### Removed

- YAML preloaded queries support (`--preloaded-queries` flag and `RATEL_PRELOADED_QUERIES` env var)
- `preloaded-queries.example.yaml` file

## Previous Changes

For changes prior to this changelog, please refer to the
[commit history](https://github.com/dgraph-io/ratel/commits/main).
43 changes: 43 additions & 0 deletions INSTRUCTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,49 @@ make test
./scripts/build.prod.sh --version 20.04.1
```

## Saved Queries

Ratel supports saving queries to a SQLite database for persistence and sharing. Queries can be
saved, edited, and deleted directly from the UI using the "Saved" dropdown button.

### Configuration

The saved queries database can be configured using either:

- **Command-line flag**: `--queries-db /path/to/queries.db`
- **Environment variable**: `RATEL_QUERIES_DB=/path/to/queries.db`

The flag takes precedence over the environment variable. If neither is specified, the database is
created in the per-user config directory (e.g. `~/.config/ratel/queries.db` on Linux,
`~/Library/Application Support/ratel/queries.db` on macOS), falling back to the system temp
directory only if that location is unavailable.

### Docker Usage

To persist saved queries when using Docker, mount a volume for the database file:

```yaml
dgraph-ratel:
image: dgraph/ratel:latest
ports:
- "8000:8000"
environment:
- RATEL_QUERIES_DB=/data/queries.db
volumes:
- ./ratel-data:/data
```

### API Endpoints

The saved queries feature exposes the following REST API:

| Method | Endpoint | Description |
| ------ | ------------------------ | ------------------------ |
| GET | `/api/saved-queries` | List all saved queries |
| POST | `/api/saved-queries` | Create a new query |
| PUT | `/api/saved-queries/:id` | Update an existing query |
| DELETE | `/api/saved-queries/:id` | Delete a query |

## Serving over HTTPS

By default Ratel will serve the UI over HTTP. You can switch to serve the UI with **only** HTTPS by
Expand Down
27 changes: 18 additions & 9 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

222 changes: 222 additions & 0 deletions client/src/actions/savedQueries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/*
* SPDX-FileCopyrightText: © Hypermode Inc. <hello@hypermode.com>
* SPDX-License-Identifier: Apache-2.0
*/

import { updateAction, updateQuery } from 'actions/query'

// Fetch actions
export const FETCH_SAVED_QUERIES_START = 'savedQueries/FETCH_START'
export const FETCH_SAVED_QUERIES_SUCCESS = 'savedQueries/FETCH_SUCCESS'
export const FETCH_SAVED_QUERIES_ERROR = 'savedQueries/FETCH_ERROR'

// CRUD actions
export const CREATE_QUERY_START = 'savedQueries/CREATE_START'
export const CREATE_QUERY_SUCCESS = 'savedQueries/CREATE_SUCCESS'
export const CREATE_QUERY_ERROR = 'savedQueries/CREATE_ERROR'

export const UPDATE_QUERY_START = 'savedQueries/UPDATE_START'
export const UPDATE_QUERY_SUCCESS = 'savedQueries/UPDATE_SUCCESS'
export const UPDATE_QUERY_ERROR = 'savedQueries/UPDATE_ERROR'

export const DELETE_QUERY_START = 'savedQueries/DELETE_START'
export const DELETE_QUERY_SUCCESS = 'savedQueries/DELETE_SUCCESS'
export const DELETE_QUERY_ERROR = 'savedQueries/DELETE_ERROR'

// UI actions
export const OPEN_SAVE_MODAL = 'savedQueries/OPEN_SAVE_MODAL'
export const CLOSE_SAVE_MODAL = 'savedQueries/CLOSE_SAVE_MODAL'
export const UPDATE_SAVE_FORM = 'savedQueries/UPDATE_SAVE_FORM'

// Fetch all saved queries
export function fetchSavedQueries() {
return async (dispatch) => {
dispatch({ type: FETCH_SAVED_QUERIES_START })

try {
const response = await fetch('/api/saved-queries')
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const data = await response.json()

dispatch({
type: FETCH_SAVED_QUERIES_SUCCESS,
payload: data,
})
} catch (error) {
dispatch({
type: FETCH_SAVED_QUERIES_ERROR,
error: error.message,
})
}
}
}

// Create a new saved query
export function createSavedQuery(queryData) {
return async (dispatch) => {
dispatch({ type: CREATE_QUERY_START })

try {
const response = await fetch('/api/saved-queries', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(queryData),
})

if (!response.ok) {
const data = await response.json()
throw new Error(data.error || `HTTP ${response.status}`)
}

const query = await response.json()

dispatch({
type: CREATE_QUERY_SUCCESS,
query,
})

dispatch({ type: CLOSE_SAVE_MODAL })

return query
} catch (error) {
dispatch({
type: CREATE_QUERY_ERROR,
error: error.message,
})
throw error
}
}
}

// Update an existing saved query
export function updateSavedQuery(id, queryData) {
return async (dispatch) => {
dispatch({ type: UPDATE_QUERY_START })

try {
const response = await fetch(`/api/saved-queries/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(queryData),
})

if (!response.ok) {
const data = await response.json()
throw new Error(data.error || `HTTP ${response.status}`)
}

const query = await response.json()

dispatch({
type: UPDATE_QUERY_SUCCESS,
query,
})

dispatch({ type: CLOSE_SAVE_MODAL })

return query
} catch (error) {
dispatch({
type: UPDATE_QUERY_ERROR,
error: error.message,
})
throw error
}
}
}

// Delete a saved query
export function deleteSavedQuery(id) {
return async (dispatch) => {
dispatch({ type: DELETE_QUERY_START, id })

try {
const response = await fetch(`/api/saved-queries/${id}`, {
method: 'DELETE',
})

if (!response.ok && response.status !== 204) {
const data = await response.json()
throw new Error(data.error || `HTTP ${response.status}`)
}

dispatch({
type: DELETE_QUERY_SUCCESS,
id,
})
} catch (error) {
dispatch({
type: DELETE_QUERY_ERROR,
error: error.message,
})
throw error
}
}
}

// Select a saved query (load into editor without running)
export function selectSavedQuery(query) {
return (dispatch) => {
// Load query into editor
dispatch(updateQuery(query.query))
dispatch(updateAction(query.action))
}
}

// Open save modal (for new query or editing existing)
export function openSaveModal(editingQuery = null) {
return (dispatch, getState) => {
const { query } = getState()

dispatch({
type: OPEN_SAVE_MODAL,
editingQuery,
// Pre-fill form with current editor content if new, or existing query data if editing
formData: editingQuery
? {
name: editingQuery.name,
description: editingQuery.description || '',
category: editingQuery.category || 'General',
action: editingQuery.action || 'query',
query: editingQuery.query,
}
: {
name: '',
description: '',
category: 'General',
action: query.action || 'query',
query: query.query || '',
},
})
}
}

// Close save modal
export function closeSaveModal() {
return { type: CLOSE_SAVE_MODAL }
}

// Update form field in save modal
export function updateSaveForm(field, value) {
return {
type: UPDATE_SAVE_FORM,
field,
value,
}
}

// Save current query (create or update)
export function saveCurrentQuery() {
return (dispatch, getState) => {
const { savedQueries } = getState()
const { editingQuery, saveForm } = savedQueries

if (editingQuery) {
return dispatch(updateSavedQuery(editingQuery.id, saveForm))
} else {
return dispatch(createSavedQuery(saveForm))
}
}
}
Loading