feat(desktop): add redis gui foundation baseline
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[resolver]
|
||||||
|
incompatible-rust-versions = "fallback"
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
target/
|
||||||
|
package-lock.json
|
||||||
|
.DS_Store
|
||||||
4933
Cargo.lock
generated
Normal file
4933
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"apps/desktop/src-tauri",
|
||||||
|
"crates/redis-core",
|
||||||
|
]
|
||||||
|
default-members = ["crates/redis-core"]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
edition = "2024"
|
||||||
|
license = "MIT"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
thiserror = "2.0"
|
||||||
|
|
||||||
83
README.md
Normal file
83
README.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Redis GUI Foundation
|
||||||
|
|
||||||
|
Redis GUI 的产品基础工程,采用 Rust workspace + Tauri desktop 结构,为后续连接管理、命令执行、键浏览和危险操作防护提供稳定后端边界。
|
||||||
|
|
||||||
|
## Workspace Layout
|
||||||
|
|
||||||
|
- `apps/desktop`: Tauri 桌面入口与 React + Tailwind 工作台壳层。
|
||||||
|
- `crates/redis-core`: 可复用的领域与应用核心,供 desktop、未来 CLI 和自动化入口共享。
|
||||||
|
- `docs/architecture.md`: 当前阶段的边界、持久化策略和演进约束。
|
||||||
|
- `docs/frontend-workspace-baseline.md`: 首版桌面信息架构、UI/UX 范围和验收要点。
|
||||||
|
|
||||||
|
## Current Backend Scope
|
||||||
|
|
||||||
|
- 建立可复用的 `redis-core` crate,承载连接模型、连接测试、key 浏览和基础命令执行能力。
|
||||||
|
- 提供 Redis 常见 value 类型的统一读取抽象:`string`、`hash`、`list`、`set`、`zset`、`stream`。
|
||||||
|
- 提供 V1 安全写入边界:仅允许现有 string key 的整值替换,并显式暴露只读类型差异。
|
||||||
|
- 提供 TTL 读取与修改能力:保留 TTL、设置毫秒级过期、移除 TTL。
|
||||||
|
- 建立 `apps/desktop/src-tauri`,使 GUI 通过统一命令面访问后端。
|
||||||
|
- 明确当前持久化边界:本阶段不落盘连接密钥,不承诺历史数据兼容。
|
||||||
|
- 对单实例 Redis 提供最小可验证的后端契约:AUTH、DB 选择、`PING`、`SCAN`/`TYPE`/`PTTL` key 元数据浏览,以及基础 RESP2 命令执行。
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Rust 1.85+
|
||||||
|
- Node.js 24+
|
||||||
|
- pnpm 10+
|
||||||
|
- Docker CLI for the local Redis smoke fixture
|
||||||
|
- Linux 开发环境需要 Tauri 运行依赖,例如 `webkit2gtk` 和 `gtk3` 开发包
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
cargo test -p redis-core
|
||||||
|
pnpm run desktop:dev
|
||||||
|
pnpm run desktop:build
|
||||||
|
pnpm run desktop:tauri:dev
|
||||||
|
pnpm run desktop:tauri:build
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 仓库现在同时补齐了 `pnpm-workspace.yaml` 和根 `package.json` 的 `packageManager`,将 `pnpm` 作为桌面开发与打包的规范入口。
|
||||||
|
- 仓库通过 `.npmrc` 固定安装 devDependencies,避免宿主机的 `NODE_ENV=production` 破坏桌面构建链路。
|
||||||
|
- `cargo test -p redis-core` 默认验证共享后端核心,包括 mock Redis 握手与命令执行测试,避免在缺少桌面系统库时阻断后端迭代。
|
||||||
|
- `pnpm run desktop:dev` 启动浏览器态工作台壳层,用于检查布局、交互和 demo fallback;此模式下没有 Tauri 命令桥。
|
||||||
|
- `pnpm run desktop:tauri:dev` 启动带 Tauri 后端桥的桌面工作台;连接测试和命令执行会调用现有 `test_redis_connection` 与 `execute_redis_command` 命令,因此需要完整的 Tauri Linux 运行依赖。
|
||||||
|
- `pnpm run desktop:tauri:build` 会先执行桌面前端 build,再进入 Tauri/Rust 的真实打包流程;当前 Linux `.deb` 与 `.rpm` 产物位于仓库根 `target/release/bundle/`。
|
||||||
|
- Linux 上进入 Tauri 打包前,可先用 `pkg-config --modversion gtk+-3.0 webkit2gtk-4.1 libsoup-3.0` 做原生依赖预检查;任一缺失都会阻断桌面包产物生成。
|
||||||
|
|
||||||
|
## Local Redis Smoke Fixture
|
||||||
|
|
||||||
|
仓库现在提供一个本地 Redis fixture 约定,用于 QA 和开发共用的 smoke 数据源:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/redis-fixture/start.sh
|
||||||
|
./scripts/redis-fixture/seed.sh
|
||||||
|
pnpm run desktop:tauri:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
默认连接参数:
|
||||||
|
|
||||||
|
- Host: `127.0.0.1`
|
||||||
|
- Port: `6380`
|
||||||
|
- Database: `0`
|
||||||
|
- Password: empty
|
||||||
|
- TLS: disabled
|
||||||
|
|
||||||
|
清理命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/redis-fixture/stop.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
完整本地 build 与 smoke 步骤见 `docs/qa/local-desktop-smoke.md`。
|
||||||
|
|
||||||
|
## Current Frontend Scope
|
||||||
|
|
||||||
|
- 首版可见界面现在已接入真实的桌面 browse / inspect / string save / TTL update 路径,但仍保持危险操作和完整产品化流程在后续阶段演进。
|
||||||
|
- 当前前端消费 `backend_bootstrap`、`test_redis_connection`、`browse_redis_keys`、`read_redis_value`、`write_redis_value` 和 `execute_redis_command`;浏览器态仍保持明确的 demo fallback。
|
||||||
|
- `inspect_redis_key` 与 `update_redis_key_ttl` 已在桌面桥可用;当前 UI 已暴露 TTL 设置与移除控制,但删除执行仍保持未开放状态。
|
||||||
|
- 连接测试、key browse、value inspector 和命令托盘在 Tauri 桌面运行时走真实后端桥;浏览器态 `pnpm run desktop:dev` 会明确回退到 demo 数据,不伪装成真实 Redis 行为。
|
||||||
|
- 具体 UI/UX 边界与 QA 验收入口见 `docs/frontend-workspace-baseline.md` 和 `docs/qa/acceptance-matrix.md`。
|
||||||
12
apps/desktop/index.html
Normal file
12
apps/desktop/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Redis GUI Foundation</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
28
apps/desktop/package.json
Normal file
28
apps/desktop/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "@redis-gui/desktop",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host 0.0.0.0 --port 1420",
|
||||||
|
"build": "vite build",
|
||||||
|
"tauri": "tauri"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@tauri-apps/api": "^2.1.0",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.544.0",
|
||||||
|
"react": "^19.1.1",
|
||||||
|
"react-dom": "^19.1.1",
|
||||||
|
"tailwind-merge": "^3.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.1.12",
|
||||||
|
"@tauri-apps/cli": "^2.1.0",
|
||||||
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
|
"tailwindcss": "^4.1.12",
|
||||||
|
"vite": "^7.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
2
apps/desktop/src-tauri/.gitignore
vendored
Normal file
2
apps/desktop/src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
|
||||||
15
apps/desktop/src-tauri/Cargo.toml
Normal file
15
apps/desktop/src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "desktop-shell"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2.0", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
redis-core = { path = "../../../crates/redis-core" }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
tauri = { version = "2.0", features = [] }
|
||||||
|
|
||||||
3
apps/desktop/src-tauri/build.rs
Normal file
3
apps/desktop/src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
12
apps/desktop/src-tauri/capabilities/default.json
Normal file
12
apps/desktop/src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Default capability for the primary desktop window.",
|
||||||
|
"windows": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"core:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
1
apps/desktop/src-tauri/gen/schemas/acl-manifests.json
Normal file
1
apps/desktop/src-tauri/gen/schemas/acl-manifests.json
Normal file
File diff suppressed because one or more lines are too long
1
apps/desktop/src-tauri/gen/schemas/capabilities.json
Normal file
1
apps/desktop/src-tauri/gen/schemas/capabilities.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"default":{"identifier":"default","description":"Default capability for the primary desktop window.","local":true,"windows":["main"],"permissions":["core:default"]}}
|
||||||
2244
apps/desktop/src-tauri/gen/schemas/desktop-schema.json
Normal file
2244
apps/desktop/src-tauri/gen/schemas/desktop-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
2244
apps/desktop/src-tauri/gen/schemas/linux-schema.json
Normal file
2244
apps/desktop/src-tauri/gen/schemas/linux-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
apps/desktop/src-tauri/icons/icon.png
Normal file
BIN
apps/desktop/src-tauri/icons/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 B |
69
apps/desktop/src-tauri/src/main.rs
Normal file
69
apps/desktop/src-tauri/src/main.rs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
use redis_core::{
|
||||||
|
BackendBootstrap, BackendError, CommandExecutionResult, ConnectionTestResult,
|
||||||
|
RedisCommandRequest, RedisConnectionRequest, RedisKeyBrowseRequest, RedisKeyBrowseResult,
|
||||||
|
RedisKeyMetadata, RedisKeyMetadataRequest, RedisKeyTtlUpdateRequest, RedisKeyTtlUpdateResult,
|
||||||
|
RedisValueReadRequest, RedisValueRecord, RedisValueWriteRequest, browse_keys, execute_command,
|
||||||
|
inspect_key, read_value, test_connection, update_key_ttl, write_value,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn backend_bootstrap() -> BackendBootstrap {
|
||||||
|
BackendBootstrap::for_desktop()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn test_redis_connection(
|
||||||
|
request: RedisConnectionRequest,
|
||||||
|
) -> Result<ConnectionTestResult, BackendError> {
|
||||||
|
test_connection(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn execute_redis_command(
|
||||||
|
request: RedisCommandRequest,
|
||||||
|
) -> Result<CommandExecutionResult, BackendError> {
|
||||||
|
execute_command(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn browse_redis_keys(request: RedisKeyBrowseRequest) -> Result<RedisKeyBrowseResult, BackendError> {
|
||||||
|
browse_keys(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn inspect_redis_key(request: RedisKeyMetadataRequest) -> Result<RedisKeyMetadata, BackendError> {
|
||||||
|
inspect_key(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn read_redis_value(request: RedisValueReadRequest) -> Result<RedisValueRecord, BackendError> {
|
||||||
|
read_value(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn write_redis_value(request: RedisValueWriteRequest) -> Result<RedisValueRecord, BackendError> {
|
||||||
|
write_value(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn update_redis_key_ttl(
|
||||||
|
request: RedisKeyTtlUpdateRequest,
|
||||||
|
) -> Result<RedisKeyTtlUpdateResult, BackendError> {
|
||||||
|
update_key_ttl(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
backend_bootstrap,
|
||||||
|
test_redis_connection,
|
||||||
|
execute_redis_command,
|
||||||
|
browse_redis_keys,
|
||||||
|
inspect_redis_key,
|
||||||
|
read_redis_value,
|
||||||
|
write_redis_value,
|
||||||
|
update_redis_key_ttl
|
||||||
|
])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("failed to run desktop shell");
|
||||||
|
}
|
||||||
30
apps/desktop/src-tauri/tauri.conf.json
Normal file
30
apps/desktop/src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "Redis GUI Foundation",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "ing.paperclip.redis-gui-foundation",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
"beforeBuildCommand": "pnpm build",
|
||||||
|
"devUrl": "http://localhost:1420",
|
||||||
|
"frontendDist": "../dist"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"label": "main",
|
||||||
|
"title": "Redis GUI Foundation",
|
||||||
|
"width": 1280,
|
||||||
|
"height": 820,
|
||||||
|
"resizable": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all"
|
||||||
|
}
|
||||||
|
}
|
||||||
2326
apps/desktop/src/App.tsx
Normal file
2326
apps/desktop/src/App.tsx
Normal file
File diff suppressed because it is too large
Load Diff
32
apps/desktop/src/components/ui/badge.tsx
Normal file
32
apps/desktop/src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center rounded-full border px-2.5 py-1 font-mono text-[11px] uppercase tracking-[0.18em]",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
neutral:
|
||||||
|
"border-[var(--line-soft)] bg-[var(--surface-base)] text-[var(--ink-muted)]",
|
||||||
|
accent:
|
||||||
|
"border-[var(--accent-strong)] bg-[var(--accent-faint)] text-[var(--accent-strong)]",
|
||||||
|
danger:
|
||||||
|
"border-[var(--danger-soft)] bg-[var(--danger-faint)] text-[var(--danger-strong)]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "neutral",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge };
|
||||||
51
apps/desktop/src/components/ui/button.tsx
Normal file
51
apps/desktop/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 rounded-full border text-sm font-medium transition outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent-strong)] focus-visible:ring-offset-2 focus-visible:ring-offset-transparent disabled:pointer-events-none disabled:opacity-45",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-[var(--accent-strong)] bg-[var(--accent-strong)] text-white hover:opacity-92",
|
||||||
|
outline:
|
||||||
|
"border-[var(--line-soft)] bg-[var(--surface-base)] text-[var(--ink-0)] hover:border-[var(--line-strong)]",
|
||||||
|
subtle:
|
||||||
|
"border-[var(--line-soft)] bg-[var(--surface-muted)] text-[var(--ink-0)] hover:border-[var(--line-strong)]",
|
||||||
|
danger:
|
||||||
|
"border-[var(--danger-soft)] bg-[var(--danger-strong)] text-white hover:opacity-92",
|
||||||
|
ghost:
|
||||||
|
"border-transparent bg-transparent text-[var(--ink-soft)] hover:border-[var(--line-soft)] hover:bg-[var(--surface-base)] hover:text-[var(--ink-0)]",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "px-4 py-2.5",
|
||||||
|
sm: "px-3 py-2",
|
||||||
|
lg: "px-5 py-3",
|
||||||
|
icon: "h-10 w-10 rounded-2xl p-0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, type = "button", ...props }, ref) => (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
type={type}
|
||||||
|
className={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Button.displayName = "Button";
|
||||||
|
|
||||||
|
export { Button, buttonVariants };
|
||||||
102
apps/desktop/src/components/ui/dialog.tsx
Normal file
102
apps/desktop/src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root;
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger;
|
||||||
|
const DialogPortal = DialogPrimitive.Portal;
|
||||||
|
const DialogClose = DialogPrimitive.Close;
|
||||||
|
|
||||||
|
const DialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-[color:color-mix(in_srgb,var(--ink-0)_24%,transparent)] backdrop-blur-[2px]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
|
const DialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-1/2 top-1/2 z-50 grid w-[min(100%-2rem,44rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-[28px] border border-[var(--line-soft)] bg-[var(--surface-raised)] p-6 shadow-[var(--shadow-panel)] outline-none",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-2xl border border-[var(--line-soft)] bg-[var(--surface-base)] p-2 text-[var(--ink-muted)] transition hover:border-[var(--line-strong)] hover:text-[var(--ink-0)]">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
));
|
||||||
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
function DialogHeader({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return <div className={cn("flex flex-col gap-2", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogFooter({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("flex flex-wrap justify-end gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-2xl font-semibold tracking-[-0.03em] text-[var(--ink-0)]", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||||
|
|
||||||
|
const DialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm leading-6 text-[var(--ink-soft)]", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
};
|
||||||
18
apps/desktop/src/components/ui/input.tsx
Normal file
18
apps/desktop/src/components/ui/input.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-11 w-full rounded-2xl border border-[var(--line-soft)] bg-[var(--surface-base)] px-3 py-2 text-sm text-[var(--ink-0)] outline-none transition placeholder:text-[var(--ink-muted)] focus-visible:border-[var(--accent-strong)] focus-visible:ring-2 focus-visible:ring-[color:color-mix(in_srgb,var(--accent-strong)_18%,transparent)] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Input.displayName = "Input";
|
||||||
|
|
||||||
|
export { Input };
|
||||||
19
apps/desktop/src/components/ui/textarea.tsx
Normal file
19
apps/desktop/src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
|
const Textarea = React.forwardRef<
|
||||||
|
HTMLTextAreaElement,
|
||||||
|
React.ComponentProps<"textarea">
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<textarea
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-28 w-full rounded-[22px] border border-[var(--line-soft)] bg-[var(--surface-base)] px-4 py-3 text-sm text-[var(--ink-0)] outline-none transition placeholder:text-[var(--ink-muted)] focus-visible:border-[var(--accent-strong)] focus-visible:ring-2 focus-visible:ring-[color:color-mix(in_srgb,var(--accent-strong)_18%,transparent)] disabled:cursor-not-allowed disabled:opacity-60",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Textarea.displayName = "Textarea";
|
||||||
|
|
||||||
|
export { Textarea };
|
||||||
79
apps/desktop/src/index.css
Normal file
79
apps/desktop/src/index.css
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
font-family: "IBM Plex Sans", "Aptos", "Segoe UI", sans-serif;
|
||||||
|
--surface-base: #f3f0ea;
|
||||||
|
--surface-muted: #ebe6de;
|
||||||
|
--surface-raised: #fbfaf7;
|
||||||
|
--surface-strong: #f1ede7;
|
||||||
|
--line-soft: #d9d0c4;
|
||||||
|
--line-strong: #b5a897;
|
||||||
|
--ink-0: #1f1d1a;
|
||||||
|
--ink-soft: #4f4a44;
|
||||||
|
--ink-muted: #7f766d;
|
||||||
|
--accent-strong: #1f5c52;
|
||||||
|
--accent-faint: #dcece7;
|
||||||
|
--danger-strong: #9b3b2e;
|
||||||
|
--danger-soft: #d7a39a;
|
||||||
|
--danger-faint: #f6e5e1;
|
||||||
|
--shadow-panel: 0 18px 48px rgba(34, 29, 25, 0.09);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(247, 243, 236, 0.96) 0%, rgba(238, 232, 222, 0.92) 48%, rgba(232, 226, 214, 0.9) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--surface-base: #181715;
|
||||||
|
--surface-muted: #201e1a;
|
||||||
|
--surface-raised: #24221e;
|
||||||
|
--surface-strong: #2d2a25;
|
||||||
|
--line-soft: #433f38;
|
||||||
|
--line-strong: #5f584d;
|
||||||
|
--ink-0: #f1ece4;
|
||||||
|
--ink-soft: #d2c8bc;
|
||||||
|
--ink-muted: #9e9487;
|
||||||
|
--accent-strong: #7ec2b5;
|
||||||
|
--accent-faint: rgba(71, 124, 114, 0.22);
|
||||||
|
--danger-strong: #f08c7d;
|
||||||
|
--danger-soft: rgba(210, 110, 92, 0.42);
|
||||||
|
--danger-faint: rgba(151, 63, 49, 0.18);
|
||||||
|
--shadow-panel: 0 20px 56px rgba(0, 0, 0, 0.32);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(35, 32, 29, 0.98) 0%, rgba(21, 20, 18, 0.96) 52%, rgba(16, 15, 13, 0.98) 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ink-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder,
|
||||||
|
textarea::placeholder {
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: color-mix(in srgb, var(--accent-strong) 24%, transparent);
|
||||||
|
}
|
||||||
6
apps/desktop/src/lib/utils.ts
Normal file
6
apps/desktop/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
10
apps/desktop/src/main.tsx
Normal file
10
apps/desktop/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import App from "./App";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
11
apps/desktop/vite.config.ts
Normal file
11
apps/desktop/vite.config.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import desktopPackage from "./package.json";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
define: {
|
||||||
|
__APP_VERSION__: JSON.stringify(desktopPackage.version),
|
||||||
|
},
|
||||||
|
});
|
||||||
7
crates/redis-core/Cargo.lock
generated
Normal file
7
crates/redis-core/Cargo.lock
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redis-core"
|
||||||
|
version = "0.1.0"
|
||||||
9
crates/redis-core/Cargo.toml
Normal file
9
crates/redis-core/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "redis-core"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
54
crates/redis-core/src/bootstrap.rs
Normal file
54
crates/redis-core/src/bootstrap.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct BackendBootstrap {
|
||||||
|
pub app_name: String,
|
||||||
|
pub surface: EntrySurface,
|
||||||
|
pub persistence: PersistenceBoundary,
|
||||||
|
pub command_execution: String,
|
||||||
|
pub key_browsing: String,
|
||||||
|
pub next_backend_milestone: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackendBootstrap {
|
||||||
|
pub fn for_desktop() -> Self {
|
||||||
|
Self {
|
||||||
|
app_name: "Redis GUI Foundation".to_string(),
|
||||||
|
surface: EntrySurface::Desktop,
|
||||||
|
persistence: PersistenceBoundary {
|
||||||
|
connection_catalog: "not_configured".to_string(),
|
||||||
|
secrets: SecretHandling::MemoryOnly.as_str().to_string(),
|
||||||
|
},
|
||||||
|
command_execution: "single_instance_enabled".to_string(),
|
||||||
|
key_browsing: "scan_metadata_enabled".to_string(),
|
||||||
|
next_backend_milestone: "value_surface_and_catalog_persistence".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum EntrySurface {
|
||||||
|
Desktop,
|
||||||
|
Cli,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct PersistenceBoundary {
|
||||||
|
pub connection_catalog: String,
|
||||||
|
pub secrets: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum SecretHandling {
|
||||||
|
MemoryOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SecretHandling {
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::MemoryOnly => "memory_only",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
238
crates/redis-core/src/connection.rs
Normal file
238
crates/redis-core/src/connection.rs
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::BackendError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct ConnectionProfileDraft {
|
||||||
|
pub display_name: String,
|
||||||
|
pub target: ConnectionTarget,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct ConnectionTarget {
|
||||||
|
pub host: String,
|
||||||
|
pub port: u16,
|
||||||
|
pub database: i64,
|
||||||
|
pub username: Option<String>,
|
||||||
|
pub tls_mode: TlsMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ConnectionTarget {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
host: "127.0.0.1".to_string(),
|
||||||
|
port: 6379,
|
||||||
|
database: 0,
|
||||||
|
username: None,
|
||||||
|
tls_mode: TlsMode::Disabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectionTarget {
|
||||||
|
pub fn validate(&self) -> Result<(), BackendError> {
|
||||||
|
if self.host.trim().is_empty() {
|
||||||
|
return Err(BackendError::invalid_connection_config(
|
||||||
|
"Redis host must not be empty.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.database < 0 {
|
||||||
|
return Err(BackendError::invalid_connection_config(
|
||||||
|
"Redis database must be zero or a positive integer.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RedisConnectionRequest {
|
||||||
|
pub target: ConnectionTarget,
|
||||||
|
pub password: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct ConnectionTestResult {
|
||||||
|
pub selected_database: i64,
|
||||||
|
pub authenticated_as: Option<String>,
|
||||||
|
pub round_trip_status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RedisCommandRequest {
|
||||||
|
pub connection: RedisConnectionRequest,
|
||||||
|
pub command: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub arguments: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct CommandExecutionResult {
|
||||||
|
pub command: String,
|
||||||
|
pub arguments: Vec<String>,
|
||||||
|
pub database: i64,
|
||||||
|
pub response: RedisResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RedisKeyBrowseRequest {
|
||||||
|
pub connection: RedisConnectionRequest,
|
||||||
|
#[serde(default = "default_scan_cursor")]
|
||||||
|
pub cursor: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub pattern: Option<String>,
|
||||||
|
#[serde(default = "default_scan_page_size")]
|
||||||
|
pub page_size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RedisKeyBrowseResult {
|
||||||
|
pub database: i64,
|
||||||
|
pub submitted_cursor: String,
|
||||||
|
pub next_cursor: String,
|
||||||
|
pub has_more: bool,
|
||||||
|
pub page_size: u32,
|
||||||
|
pub pattern: Option<String>,
|
||||||
|
pub keys: Vec<RedisKeyMetadata>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RedisKeyMetadataRequest {
|
||||||
|
pub connection: RedisConnectionRequest,
|
||||||
|
pub key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RedisValueReadRequest {
|
||||||
|
pub connection: RedisConnectionRequest,
|
||||||
|
pub key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RedisValueWriteRequest {
|
||||||
|
pub connection: RedisConnectionRequest,
|
||||||
|
pub key: String,
|
||||||
|
pub value: RedisValueWriteInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RedisKeyTtlUpdateRequest {
|
||||||
|
pub connection: RedisConnectionRequest,
|
||||||
|
pub key: String,
|
||||||
|
pub operation: RedisKeyTtlUpdate,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RedisKeyMetadata {
|
||||||
|
pub name: String,
|
||||||
|
pub database: i64,
|
||||||
|
pub exists: bool,
|
||||||
|
pub key_type: String,
|
||||||
|
pub ttl: RedisKeyTtl,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
|
pub enum RedisKeyTtl {
|
||||||
|
Persistent,
|
||||||
|
Missing,
|
||||||
|
ExpiresInMillis { value: i64 },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RedisValueRecord {
|
||||||
|
pub metadata: RedisKeyMetadata,
|
||||||
|
pub data: RedisValueData,
|
||||||
|
pub write_capability: RedisValueWriteCapability,
|
||||||
|
pub ttl_write_supported: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RedisKeyTtlUpdateResult {
|
||||||
|
pub metadata: RedisKeyMetadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RedisFieldValueEntry {
|
||||||
|
pub field: RedisValueContent,
|
||||||
|
pub value: RedisValueContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RedisSortedSetEntry {
|
||||||
|
pub member: RedisValueContent,
|
||||||
|
pub score: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RedisStreamEntry {
|
||||||
|
pub id: String,
|
||||||
|
pub fields: Vec<RedisFieldValueEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
|
pub enum RedisValueContent {
|
||||||
|
String { value: String },
|
||||||
|
Binary { bytes: Vec<u8> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
|
pub enum RedisValueData {
|
||||||
|
Missing,
|
||||||
|
String { value: RedisValueContent },
|
||||||
|
Hash { entries: Vec<RedisFieldValueEntry> },
|
||||||
|
List { items: Vec<RedisValueContent> },
|
||||||
|
Set { members: Vec<RedisValueContent> },
|
||||||
|
SortedSet { entries: Vec<RedisSortedSetEntry> },
|
||||||
|
Stream { entries: Vec<RedisStreamEntry> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
|
pub enum RedisValueWriteInput {
|
||||||
|
String { value: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum RedisValueWriteCapability {
|
||||||
|
None,
|
||||||
|
ReplaceString,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
|
pub enum RedisKeyTtlUpdate {
|
||||||
|
Persist,
|
||||||
|
ExpiresInMillis { value: u64 },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
|
pub enum RedisResponse {
|
||||||
|
Null,
|
||||||
|
Integer { value: i64 },
|
||||||
|
String { value: String },
|
||||||
|
Binary { bytes: Vec<u8> },
|
||||||
|
Array { items: Vec<RedisResponse> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum TlsMode {
|
||||||
|
Disabled,
|
||||||
|
Preferred,
|
||||||
|
Required,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_scan_cursor() -> String {
|
||||||
|
"0".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_scan_page_size() -> u32 {
|
||||||
|
100
|
||||||
|
}
|
||||||
60
crates/redis-core/src/error.rs
Normal file
60
crates/redis-core/src/error.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum BackendErrorCode {
|
||||||
|
InvalidConnectionConfig,
|
||||||
|
UnsupportedTlsMode,
|
||||||
|
UnsupportedOperation,
|
||||||
|
ConnectionFailed,
|
||||||
|
AuthenticationFailed,
|
||||||
|
CommandFailed,
|
||||||
|
Internal,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Error)]
|
||||||
|
#[error("{message}")]
|
||||||
|
pub struct BackendError {
|
||||||
|
pub code: BackendErrorCode,
|
||||||
|
pub message: String,
|
||||||
|
pub detail: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackendError {
|
||||||
|
pub fn invalid_connection_config(message: impl Into<String>) -> Self {
|
||||||
|
Self::new(BackendErrorCode::InvalidConnectionConfig, message, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unsupported_tls_mode(message: impl Into<String>) -> Self {
|
||||||
|
Self::new(BackendErrorCode::UnsupportedTlsMode, message, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connection_failed(message: impl Into<String>, detail: Option<String>) -> Self {
|
||||||
|
Self::new(BackendErrorCode::ConnectionFailed, message, detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unsupported_operation(message: impl Into<String>, detail: Option<String>) -> Self {
|
||||||
|
Self::new(BackendErrorCode::UnsupportedOperation, message, detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn authentication_failed(message: impl Into<String>, detail: Option<String>) -> Self {
|
||||||
|
Self::new(BackendErrorCode::AuthenticationFailed, message, detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn command_failed(message: impl Into<String>, detail: Option<String>) -> Self {
|
||||||
|
Self::new(BackendErrorCode::CommandFailed, message, detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn internal(message: impl Into<String>, detail: Option<String>) -> Self {
|
||||||
|
Self::new(BackendErrorCode::Internal, message, detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(code: BackendErrorCode, message: impl Into<String>, detail: Option<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
code,
|
||||||
|
message: message.into(),
|
||||||
|
detail,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
crates/redis-core/src/lib.rs
Normal file
45
crates/redis-core/src/lib.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
mod bootstrap;
|
||||||
|
mod connection;
|
||||||
|
mod error;
|
||||||
|
mod service;
|
||||||
|
|
||||||
|
pub use bootstrap::{BackendBootstrap, EntrySurface, PersistenceBoundary, SecretHandling};
|
||||||
|
pub use connection::{
|
||||||
|
CommandExecutionResult, ConnectionProfileDraft, ConnectionTarget, ConnectionTestResult,
|
||||||
|
RedisCommandRequest, RedisConnectionRequest, RedisFieldValueEntry, RedisKeyBrowseRequest,
|
||||||
|
RedisKeyBrowseResult, RedisKeyMetadata, RedisKeyMetadataRequest, RedisKeyTtl,
|
||||||
|
RedisKeyTtlUpdate, RedisKeyTtlUpdateRequest, RedisKeyTtlUpdateResult, RedisResponse,
|
||||||
|
RedisSortedSetEntry, RedisStreamEntry, RedisValueContent, RedisValueData,
|
||||||
|
RedisValueReadRequest, RedisValueRecord, RedisValueWriteCapability, RedisValueWriteInput,
|
||||||
|
RedisValueWriteRequest, TlsMode,
|
||||||
|
};
|
||||||
|
pub use error::{BackendError, BackendErrorCode};
|
||||||
|
pub use service::{
|
||||||
|
browse_keys, execute_command, inspect_key, read_value, test_connection, update_key_ttl,
|
||||||
|
write_value,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{BackendBootstrap, ConnectionTarget, TlsMode};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn desktop_bootstrap_keeps_persistence_guardrails_stable() {
|
||||||
|
let bootstrap = BackendBootstrap::for_desktop();
|
||||||
|
|
||||||
|
assert_eq!(bootstrap.persistence.connection_catalog, "not_configured");
|
||||||
|
assert_eq!(bootstrap.persistence.secrets, "memory_only");
|
||||||
|
assert_eq!(bootstrap.command_execution, "single_instance_enabled");
|
||||||
|
assert_eq!(bootstrap.key_browsing, "scan_metadata_enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn connection_target_defaults_match_local_redis_expectation() {
|
||||||
|
let target = ConnectionTarget::default();
|
||||||
|
|
||||||
|
assert_eq!(target.host, "127.0.0.1");
|
||||||
|
assert_eq!(target.port, 6379);
|
||||||
|
assert_eq!(target.database, 0);
|
||||||
|
assert_eq!(target.tls_mode, TlsMode::Disabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
1741
crates/redis-core/src/service.rs
Normal file
1741
crates/redis-core/src/service.rs
Normal file
File diff suppressed because it is too large
Load Diff
92
docs/architecture.md
Normal file
92
docs/architecture.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Backend Foundation
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- 使用 Rust workspace 作为产品的长期结构。
|
||||||
|
- 将可复用业务能力放入 `crates/redis-core`,避免把 Redis 语义散落在 GUI 层。
|
||||||
|
- 让 desktop 壳层只负责窗口与命令桥接,不负责领域规则。
|
||||||
|
|
||||||
|
## Current Domain Model
|
||||||
|
|
||||||
|
当前只定义启动和连接建模所需的最小领域对象:
|
||||||
|
|
||||||
|
- `ConnectionTarget`: Redis 目标地址、端口、数据库序号、用户名和 TLS 模式。
|
||||||
|
- `ConnectionProfileDraft`: GUI 或未来 CLI 收集到的连接表单草稿。
|
||||||
|
- `RedisConnectionRequest`: 一次请求内使用的连接上下文,包含非持久化密码。
|
||||||
|
- `RedisKeyBrowseRequest`: key 浏览请求,显式携带 `SCAN` cursor、pattern 与 page size。
|
||||||
|
- `RedisKeyMetadataRequest`: 单个 key 的元数据读取请求,用于 inspector 刷新与删除前确认。
|
||||||
|
- `RedisValueReadRequest`: 单个 key 的 typed value 读取请求。
|
||||||
|
- `RedisValueWriteRequest`: 单个 key 的 value 写入请求;当前仅允许 string 整值替换。
|
||||||
|
- `RedisKeyTtlUpdateRequest`: 单个 key 的 TTL 修改请求,支持设置过期时间和移除 TTL。
|
||||||
|
- `RedisCommandRequest`: 基础命令执行请求,显式拆分命令名与参数。
|
||||||
|
- `RedisKeyMetadata`: key 浏览与 inspector 共享的稳定元数据结构,覆盖存在性、类型和 TTL。
|
||||||
|
- `RedisValueRecord`: inspector 使用的稳定结构,组合 key 元数据、typed value 数据、value 写入能力和 TTL 修改能力。
|
||||||
|
- `RedisValueData`: typed value 返回体,覆盖 `string/hash/list/set/zset/stream` 与缺失 key。
|
||||||
|
- `RedisValueWriteCapability`: 当前明确区分 `replace_string` 与 `none`,让入口层知道哪些类型仍是只读。
|
||||||
|
- `CommandExecutionResult`: 以稳定的 RESP2 派生结构返回基础命令结果。
|
||||||
|
- `BackendError`: 结构化错误码,区分连接配置、认证、连接失败和命令失败。
|
||||||
|
- `BackendBootstrap`: 向入口层暴露当前支持的能力边界和安全约束。
|
||||||
|
|
||||||
|
这些对象构成当前阶段的稳定后端契约:
|
||||||
|
|
||||||
|
- 单实例连接测试通过 `PING` 验证回路。
|
||||||
|
- 认证通过请求态 `AUTH` 握手完成。
|
||||||
|
- DB 切换在连接建立后通过 `SELECT` 保证。
|
||||||
|
- 命令执行按 RESP2 基础类型映射为稳定结构体,避免 GUI 直接处理原始 socket 数据。
|
||||||
|
- key 浏览按 `SCAN` 分页返回,并为每个命中的 key 补充 `TYPE` 与 `PTTL` 元数据。
|
||||||
|
- inspector 可对单个 key 单独刷新元数据,避免 UI 为了刷新单项而重跑整页浏览。
|
||||||
|
- typed value surface 当前覆盖 `string/hash/list/set/zset/stream` 六类 Redis 数据,并显式保留类型差异。
|
||||||
|
- V1 value 编辑只允许已有 string key 的整值替换;非 string key 保持只读,避免错误覆盖集合类型。
|
||||||
|
- string 写入会保留原 TTL,TTL 修改独立通过 `PERSIST` / `PEXPIRE` 语义建模。
|
||||||
|
|
||||||
|
## Persistence Boundary
|
||||||
|
|
||||||
|
当前阶段明确保持以下约束:
|
||||||
|
|
||||||
|
- 连接密钥和密码不落盘。
|
||||||
|
- 密码仅存在于单次 `RedisConnectionRequest` 的内存生命周期内。
|
||||||
|
- 连接配置目录和迁移策略尚未启用,因此不存在历史数据兼容承诺。
|
||||||
|
- GUI 仅消费 `redis-core` 导出的稳定结构体,不直接定义持久化格式。
|
||||||
|
|
||||||
|
后续进入产品化时,应新增独立的存储 crate,并补充:
|
||||||
|
|
||||||
|
- 配置文件 schema
|
||||||
|
- 版本字段与迁移器
|
||||||
|
- 敏感信息存储策略
|
||||||
|
- 向前/向后兼容测试
|
||||||
|
|
||||||
|
## Compatibility Notes
|
||||||
|
|
||||||
|
- 当前后端只覆盖单实例 Redis 的同步 RESP2 交互。
|
||||||
|
- workspace 通过 `.cargo/config.toml` 启用 `resolver.incompatible-rust-versions = "fallback"`,避免在 `rustc 1.85` 环境下把 Tauri 传递依赖解析到更高 MSRV 的版本带。
|
||||||
|
- key 浏览当前基于 Redis `SCAN` + 每 key `TYPE`/`PTTL` 的顺序读取,优先保证正确性和契约清晰,而不是在原型期过早做 pipeline 或并发优化。
|
||||||
|
- `TlsMode::disabled` 可用,`preferred` 和 `required` 目前明确返回 `unsupported_tls_mode`,避免伪支持。
|
||||||
|
- 命令执行结果目前覆盖 RESP2 的 `simple string`、`bulk string`、`integer`、`array` 和 `null`。
|
||||||
|
- typed value 读取当前是 key 级 eager 读取:`HGETALL`、`LRANGE 0 -1`、`SMEMBERS`、`ZRANGE ... WITHSCORES`、`XRANGE - +`。这保证了契约直接,但尚未引入大 key 分页、截断或流式读取策略。
|
||||||
|
- string 写入使用事务化的 `SET ... XX` 与 TTL 恢复,避免把非 string key 误写成 string,也避免默认清空现有 TTL。
|
||||||
|
- key 元数据仍未覆盖编码、内存占用、slot、cluster 拓扑或 ACL 细粒度探测。
|
||||||
|
- 这层契约已足以支撑连接管理、基础命令台、key 浏览、value inspector 与 TTL 编辑,但还不足以覆盖 cluster、pub/sub、流式结果或长连接会话管理。
|
||||||
|
|
||||||
|
## Entry Point Contract
|
||||||
|
|
||||||
|
- Tauri 命令只暴露共享核心层的数据,不直接返回临时字符串。
|
||||||
|
- 当前桌面入口暴露八个命令:`backend_bootstrap`、`test_redis_connection`、`browse_redis_keys`、`inspect_redis_key`、`read_redis_value`、`write_redis_value`、`update_redis_key_ttl`、`execute_redis_command`。
|
||||||
|
- 任何新增入口都应优先复用 `redis-core`,保证 CLI、GUI 行为模型一致。
|
||||||
|
- 桌面入口允许演进 UI,但不应绕过领域约束自行定义 Redis 协议行为。
|
||||||
|
|
||||||
|
## Verification Notes
|
||||||
|
|
||||||
|
当前已验证的重点是:
|
||||||
|
|
||||||
|
- Rust 核心 crate 的测试与格式正确性
|
||||||
|
- Tauri desktop 命令桥可以在当前 Linux 环境完成编译检查
|
||||||
|
- Linux 下 Tauri 所需系统依赖已通过 `pkg-config` 验证
|
||||||
|
|
||||||
|
当前已完成的验证:
|
||||||
|
|
||||||
|
- `cargo test -p redis-core`
|
||||||
|
- mock Redis 测试覆盖 AUTH、SELECT、PING、SCAN 分页、TYPE、PTTL、缺失 key 元数据映射、typed value 读取、string 安全写入和 TTL 修改
|
||||||
|
- `Cargo.lock` 已重新解析到 Rust 1.85 兼容版本带:`serde_with 3.17.0`、`darling 0.21.0`、`time 0.3.44`
|
||||||
|
- `cargo check -p desktop-shell --locked`
|
||||||
|
- `pkg-config` 校验通过:`gtk+-3.0`、`webkit2gtk-4.1`、`libsoup-3.0`、`javascriptcoregtk-4.1`
|
||||||
|
- `pnpm run desktop:build`
|
||||||
123
docs/frontend-workspace-baseline.md
Normal file
123
docs/frontend-workspace-baseline.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# Redis GUI Desktop Workspace Baseline
|
||||||
|
|
||||||
|
Date: 2026-03-27
|
||||||
|
Owner: Senior Frontend Engineer
|
||||||
|
Related issues: HAC-8, HAC-9, HAC-10, HAC-11
|
||||||
|
|
||||||
|
## Current Assessment
|
||||||
|
|
||||||
|
- The repository already had a valid desktop entry at `apps/desktop` and a working Tauri bridge for `backend_bootstrap`.
|
||||||
|
- The repository also already exposed stable desktop commands for connection testing and command execution through `test_redis_connection` and `execute_redis_command`.
|
||||||
|
- The previous frontend surface was only a minimal Vite JavaScript shell and did not match the target implementation stack for downstream UI work.
|
||||||
|
- The first frontend milestone therefore needed a visible React workspace before feature delivery could continue efficiently.
|
||||||
|
|
||||||
|
## Recommended V1 UI Scope
|
||||||
|
|
||||||
|
The first visible product surface should include:
|
||||||
|
|
||||||
|
- a stable desktop shell with a persistent connection context
|
||||||
|
- a connection library and a new-connection entry point
|
||||||
|
- a database switcher that keeps the current DB visible
|
||||||
|
- a key browser with search and selection states
|
||||||
|
- a value inspector with string-edit support and explicit read-only states
|
||||||
|
- a scoped command tray that makes connection and DB context obvious
|
||||||
|
- destructive-action confirmation that repeats the key name and DB
|
||||||
|
|
||||||
|
The first visible product surface should not include:
|
||||||
|
|
||||||
|
- cluster topology
|
||||||
|
- metrics dashboards
|
||||||
|
- bulk actions
|
||||||
|
- multi-connection comparison
|
||||||
|
- full editing support for every Redis type
|
||||||
|
- privileged administrative commands
|
||||||
|
|
||||||
|
## Information Architecture
|
||||||
|
|
||||||
|
Use a four-zone workspace:
|
||||||
|
|
||||||
|
1. Utility rail
|
||||||
|
2. Browser pane
|
||||||
|
3. Inspector pane
|
||||||
|
4. Command tray
|
||||||
|
|
||||||
|
### Utility rail
|
||||||
|
|
||||||
|
- Connection library
|
||||||
|
- New connection form entry point
|
||||||
|
- Active DB switcher
|
||||||
|
- Safety posture and backend boundary summary
|
||||||
|
|
||||||
|
### Browser pane
|
||||||
|
|
||||||
|
- Search or key pattern filter
|
||||||
|
- Result count summary
|
||||||
|
- Compact key rows with type and TTL
|
||||||
|
- Empty state when no keys match
|
||||||
|
|
||||||
|
### Inspector pane
|
||||||
|
|
||||||
|
- Selected key header
|
||||||
|
- Value surface
|
||||||
|
- Save action for string keys only
|
||||||
|
- TTL control for set and persist flows
|
||||||
|
- Explicit read-only treatment for unsupported types
|
||||||
|
- Metadata and support note beside the edited surface
|
||||||
|
|
||||||
|
### Command tray
|
||||||
|
|
||||||
|
- Scoped command input
|
||||||
|
- Visible connection and DB scope
|
||||||
|
- Result history
|
||||||
|
- Hard block for destructive or administrative commands
|
||||||
|
|
||||||
|
## Visual Direction
|
||||||
|
|
||||||
|
- Calm and editorial rather than dashboard-like
|
||||||
|
- Dense enough for operator workflows without visual noise
|
||||||
|
- Structure driven by typography, separators, and restrained tinted surfaces
|
||||||
|
- Strong emphasis only for selection, state, or destructive intent
|
||||||
|
- Support both light and dark modes with low-noise neutral palettes
|
||||||
|
|
||||||
|
## Implementation Baseline Added On 2026-03-27
|
||||||
|
|
||||||
|
- `apps/desktop` now runs on React and Tailwind-based styling.
|
||||||
|
- The shell now consumes the existing Tauri bridge for `backend_bootstrap`, `test_redis_connection`, `browse_redis_keys`, `read_redis_value`, `write_redis_value`, and `execute_redis_command`.
|
||||||
|
- The inspector now consumes `update_redis_key_ttl` for live TTL set and persist actions in desktop runtime.
|
||||||
|
- shadcn-style UI primitives are used locally for buttons, inputs, textareas, badges, and dialogs.
|
||||||
|
- Browser-only dev mode uses explicit demo fallback so the visible shell can still be reviewed without a Tauri runtime.
|
||||||
|
- Tauri desktop mode now allows live connection tests, live key browsing, live value inspection, string save for supported keys, live TTL updates, and live command execution while keeping browser dev mode on explicit demo fallback.
|
||||||
|
- The visible shell now validates draft-connection name, host, port, and DB input before creation, and prevents duplicate profile names inside the local shell state.
|
||||||
|
- Zero-result key searches now place the inspector into an explicit empty state instead of leaving a stale key visible.
|
||||||
|
|
||||||
|
## QA Acceptance For This Milestone
|
||||||
|
|
||||||
|
- The shell renders four distinct zones: rail, browser, inspector, command tray.
|
||||||
|
- The header keeps app version, runtime mode, and build mode visible for smoke validation.
|
||||||
|
- The utility rail exposes a copyable QA snapshot with runtime, connection, selection, and latest command context.
|
||||||
|
- The active connection and active DB stay visible without opening a modal.
|
||||||
|
- Search filters the browser list and shows a no-results state.
|
||||||
|
- When search returns zero keys, the inspector switches to an empty state instead of showing the last selected key.
|
||||||
|
- The connection action distinguishes desktop live testing from browser demo fallback.
|
||||||
|
- Invalid draft connections cannot be created from the visible shell.
|
||||||
|
- Non-string keys are explicitly read-only.
|
||||||
|
- String keys show a save affordance near the value surface.
|
||||||
|
- TTL controls stay beside the inspector metadata and expose both set and remove flows.
|
||||||
|
- Delete requires a second confirmation step that includes key name and DB context.
|
||||||
|
- The command tray visibly blocks dangerous administrative commands.
|
||||||
|
- Command history marks whether each result came from the live desktop bridge or the demo shell.
|
||||||
|
|
||||||
|
## Run Notes
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm run desktop:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- `pnpm run desktop:dev` is the browser shell path. It supports layout review and demo fallback only.
|
||||||
|
- `pnpm run desktop:tauri:dev` is the desktop integration path. Use it when QA or backend needs to validate the existing command bridge against a live Redis target.
|
||||||
|
- Use `pnpm run desktop:build` for a production bundle check before handing the shell to QA or integration work.
|
||||||
38
docs/qa/acceptance-matrix.md
Normal file
38
docs/qa/acceptance-matrix.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Redis GUI V1 Acceptance Matrix
|
||||||
|
|
||||||
|
Date: 2026-03-27
|
||||||
|
Owner: QA Engineer
|
||||||
|
|
||||||
|
## Evidence Collected On 2026-03-27
|
||||||
|
|
||||||
|
- A fresh `cargo test -p redis-core` rerun passes in this session with 16 tests green.
|
||||||
|
- `pnpm install --lockfile-only` passes at repository root after adding `pnpm-workspace.yaml`.
|
||||||
|
- `pnpm run desktop:build` passes and emits `apps/desktop/dist/`.
|
||||||
|
- `pnpm run desktop:tauri:build` now completes Linux `.deb` and `.rpm` artifact generation under `target/release/bundle/`, then fails later in the AppImage stage with `io: Connection reset by peer (os error 104)`.
|
||||||
|
- The repository now contains a Rust workspace, a React workspace UI baseline, Tauri commands for connection testing and command execution, and shared frontend scope documentation.
|
||||||
|
- Browser dev mode still uses demo data for shell review, while Tauri desktop mode can now exercise live connection-test, key browse, value read/write, and command-execution bridges.
|
||||||
|
- A repeatable local Redis fixture and smoke runbook now exist in `scripts/redis-fixture/` and `docs/qa/local-desktop-smoke.md`.
|
||||||
|
|
||||||
|
## Matrix
|
||||||
|
|
||||||
|
| Scope | Happy Path Acceptance | Failure Path Acceptance | Required Evidence | Current Status |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| Linux app smoke | App installs or launches from a documented command, opens main window, connects to test Redis, exits cleanly | Missing Redis, bad host, bad password, unsupported DB or network loss surface actionable error and app stays stable | Launch steps, versioned artifact or dev command, screenshots, QA run log | Partially blocked: documented fixture and smoke path now exist, but a full Linux launch-and-connect evidence set is still pending |
|
||||||
|
| macOS app smoke | Same as Linux, using signed or documented macOS artifact | Same failure paths, plus first-run permission or gatekeeper guidance if relevant | Build artifact name, install steps, screenshots, QA run log | Blocked: no validated macOS build or artifact evidence |
|
||||||
|
| Windows app smoke | Same as Linux, using installer or portable package | Same failure paths, plus Windows-specific install and uninstall validation | Build artifact name, install steps, screenshots, QA run log | Blocked: no validated Windows build or artifact evidence |
|
||||||
|
| Desktop workspace shell | Rail, browser, inspector, and command tray render together with active connection, build identity, runtime mode, and a copyable QA snapshot visible; command history labels live vs demo results | Empty search state clears the inspector selection, read-only non-string value state, destructive confirmation, and browser-mode demo fallback stay stable and explicit | `npm run desktop:dev` or `npm run desktop:build`, `docs/frontend-workspace-baseline.md`, screenshots or copied QA snapshot text | Pass for shell-level evidence; not a substitute for Redis feature acceptance |
|
||||||
|
| Connection management | Create a validated draft profile, keep DB context visible, and run the existing live connection-test bridge from Tauri desktop mode | Invalid host, bad port, bad password, duplicate name, timeout, and unreachable server are handled with clear messaging | Tauri run log, screenshots, repeatable steps, Redis target | Partially blocked: the shell now validates draft name/host/port/DB locally and calls the real connection-test command, and a seeded Redis smoke path exists, but profile persistence and full evidence capture are not complete |
|
||||||
|
| Key browsing | Browse DBs and keys, paginate or virtualize large lists, view metadata | Empty DB, deleted key during browse, permission error, and connection drop do not crash UI | Seed dataset, result screenshots, observed limits | Partially blocked: desktop UI now calls the live browse bridge with `SCAN` pagination and metadata, but fixture-backed Tauri screenshots and measured limits are still pending |
|
||||||
|
| Search | Search by key name or pattern and return matching set within documented limits | No matches, invalid pattern, and very large result sets are handled predictably | Seed dataset, search queries, measured behavior | Partially blocked: desktop UI now maps search to live browse requests, but fixture-backed evidence and measured limits are still pending |
|
||||||
|
| Value editing | Open supported value types, edit, save, and refresh persisted value | Concurrent modification, invalid payload, and permission error preserve user context and surface failure | Before/after values, screenshots, repeatable steps | Partially blocked: string read/save/refresh is wired through the live desktop bridge and non-string types stay explicitly read only, but fixture-backed evidence and broader editing scope remain pending |
|
||||||
|
| TTL operations | View TTL, set TTL, remove TTL, and verify expiration behavior | Invalid TTL, already expired key, and permission error handled safely | Seed dataset, timestamps, verification steps | Partially blocked: desktop UI now exposes live TTL set and remove controls through the existing contract, but seeded-fixture validation evidence is still missing |
|
||||||
|
| Command console | Execute safe Redis commands in Tauri desktop mode and show result or error output with explicit live/demo labeling | Invalid command, long-running command, and blocked dangerous command are handled predictably | Command transcript, screenshots, allow or deny rules | Partially blocked: the desktop UI now calls the real command execution bridge, but it is not yet validated against a shared live Redis fixture and dangerous mutations still stay blocked at shell level |
|
||||||
|
| Dangerous operation confirmation | Delete, flush, overwrite, or destructive commands require explicit confirmation | Cancel path does not mutate data; confirm path mutates only intended scope | Confirmation UX screenshots, before/after data | Blocked: feature not implemented |
|
||||||
|
| Installer and packaging | Package metadata, icon, version, and uninstall behavior are correct on each target OS | Corrupt install, upgrade, downgrade, and missing dependency behavior are documented or handled | Artifact checklist, install logs, uninstall evidence | Partially blocked: Linux `.deb` and `.rpm` artifacts now exist, but AppImage failed and install or uninstall evidence is still absent |
|
||||||
|
|
||||||
|
## Current Acceptance Judgment
|
||||||
|
|
||||||
|
- No Redis-backed Redis GUI V1 feature acceptance item can be marked passed on 2026-03-27.
|
||||||
|
- Current repository state now supports shell-level frontend acceptance and partial backend feature review: the React workspace shell builds, live desktop bridges exist for connection test, browse, value read/write, and command execution, draft-validation and empty-state behavior are explicit, and shared UI scope documentation is present in-repo.
|
||||||
|
- Rust source-level test evidence is now current for `redis-core`, Linux package artifacts exist for `.deb` and `.rpm`, but live Redis screenshots and installation evidence are still required.
|
||||||
|
- QA can now reuse the documented fixture and smoke path, but feature acceptance and release sign-off remain blocked on end-to-end desktop evidence.
|
||||||
50
docs/qa/cross-platform-smoke-plan.md
Normal file
50
docs/qa/cross-platform-smoke-plan.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Cross-Platform Smoke Plan
|
||||||
|
|
||||||
|
Date: 2026-03-27
|
||||||
|
Owner: QA Engineer
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Provide one minimal but repeatable smoke flow for Windows, macOS, and Linux that proves a release candidate is installable, launchable, connectable to Redis, and safe around destructive actions.
|
||||||
|
|
||||||
|
## Preconditions Required Before Smoke Can Run
|
||||||
|
|
||||||
|
- A documented build command or signed artifact exists for each target OS.
|
||||||
|
- A seedable Redis dataset exists with strings, hashes, lists, sets, sorted sets, JSON or binary payload samples if supported, and keys with TTL.
|
||||||
|
- A documented demo or local startup path exists.
|
||||||
|
- Smoke environment variables, sample credentials, and cleanup steps are documented.
|
||||||
|
|
||||||
|
## Minimal Smoke Flow Per Platform
|
||||||
|
|
||||||
|
1. Install or launch the app from the documented artifact or dev command.
|
||||||
|
2. Verify window renders and version or build identity is visible.
|
||||||
|
3. Add a connection to the seeded Redis instance and reconnect once.
|
||||||
|
4. Browse keys and open at least one key from each supported type.
|
||||||
|
5. Search for known keys and confirm zero-result handling.
|
||||||
|
6. Edit one value, then verify persisted data through refresh or re-open.
|
||||||
|
7. Change TTL on one key, remove TTL on another, and verify the observed state.
|
||||||
|
8. Trigger one dangerous action and verify both cancel and confirm paths.
|
||||||
|
9. Open command console, run one safe read command and one invalid command.
|
||||||
|
10. Close and relaunch the app, then confirm recent connection state is consistent with product requirements.
|
||||||
|
|
||||||
|
## Failure-Path Smoke
|
||||||
|
|
||||||
|
- Start app when Redis is unreachable.
|
||||||
|
- Attempt login with bad host or credential data.
|
||||||
|
- Disconnect Redis mid-session.
|
||||||
|
- Trigger invalid value save.
|
||||||
|
- Attempt dangerous action, then cancel.
|
||||||
|
- Run unsupported or invalid command in console.
|
||||||
|
|
||||||
|
## Required Evidence
|
||||||
|
|
||||||
|
- Platform, build identifier, and execution timestamp.
|
||||||
|
- Screenshots for launch, connection success, key browse, edit save, dangerous-action confirmation, and error state.
|
||||||
|
- Exact dataset version and Redis image or binary version.
|
||||||
|
- QA log with pass, fail, blocked, or not-run result per step.
|
||||||
|
|
||||||
|
## Current Blockers On 2026-03-27
|
||||||
|
|
||||||
|
- Verified Linux package artifacts now exist for `.deb` and `.rpm` under `target/release/bundle/`, but AppImage failed in the latest run and no validated package artifact evidence exists yet for macOS or Windows.
|
||||||
|
- The repository now provides a Linux-oriented local smoke runbook and Docker-backed Redis fixture conventions, but those steps still need saved cross-platform execution evidence.
|
||||||
|
- Current desktop shell now supports live browse, inspect, string save, TTL update, and command execution in Tauri runtime, but full end-to-end smoke evidence still needs saved screenshots or run logs.
|
||||||
26
docs/qa/defect-severity.md
Normal file
26
docs/qa/defect-severity.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Defect Classification
|
||||||
|
|
||||||
|
Date: 2026-03-27
|
||||||
|
Owner: QA Engineer
|
||||||
|
|
||||||
|
## Severity
|
||||||
|
|
||||||
|
| Severity | Definition | Release Impact |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| S0 | Data loss, irreversible destructive action without confirmation, security exposure, or app cannot launch on a supported platform | Ship blocker |
|
||||||
|
| S1 | Core workflow broken: cannot connect, browse keys, search, edit values, manage TTL, or execute documented console flow | Ship blocker unless scope is formally descoped |
|
||||||
|
| S2 | Important workflow degraded with workaround, repeatable crash in non-core flow, major UI corruption, or packaging issue with workaround | Fix before release candidate or explicitly waive |
|
||||||
|
| S3 | Minor UI issue, copy issue, low-impact inconsistency, or edge-case defect with narrow reach | Can ship with tracking |
|
||||||
|
|
||||||
|
## Priority Guidance
|
||||||
|
|
||||||
|
- `critical`: active release blocker or broad regression.
|
||||||
|
- `high`: should be fixed in current milestone.
|
||||||
|
- `medium`: should be fixed after core acceptance path is stable.
|
||||||
|
- `low`: backlog candidate.
|
||||||
|
|
||||||
|
## QA Reporting Rules
|
||||||
|
|
||||||
|
- Every defect report should include environment, build id, exact steps, expected result, actual result, and evidence.
|
||||||
|
- If a defect is not reproducible, state attempts and why the result is inconclusive.
|
||||||
|
- If a defect is blocked by missing product implementation, report it as a gap, not as a functional failure.
|
||||||
87
docs/qa/local-desktop-smoke.md
Normal file
87
docs/qa/local-desktop-smoke.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Local Desktop Build And Smoke Runbook
|
||||||
|
|
||||||
|
Date: 2026-03-27
|
||||||
|
Owner: Senior Backend Engineer
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Provide one repeatable local build and smoke path that any engineer or QA can run against a seeded Redis fixture.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Rust 1.85+
|
||||||
|
- Node.js 24+
|
||||||
|
- `pnpm` 10+
|
||||||
|
- Docker CLI with permission to run containers
|
||||||
|
- Linux desktop build prerequisites for Tauri, including WebKitGTK and GTK3 development packages
|
||||||
|
|
||||||
|
## Standard Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
cargo test -p redis-core
|
||||||
|
pnpm run desktop:build
|
||||||
|
pnpm run desktop:tauri:build
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected package output path on Linux after a successful Tauri build:
|
||||||
|
|
||||||
|
```text
|
||||||
|
target/release/bundle/
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux preflight checks for native desktop libraries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pkg-config --modversion gtk+-3.0
|
||||||
|
pkg-config --modversion webkit2gtk-4.1
|
||||||
|
pkg-config --modversion libsoup-3.0
|
||||||
|
```
|
||||||
|
|
||||||
|
All three commands should return a version. If any of them fail, `pnpm run desktop:tauri:build` is still environment-blocked even if the repository scripts are correct.
|
||||||
|
|
||||||
|
## Local Redis Fixture
|
||||||
|
|
||||||
|
Start and seed a deterministic Redis instance on `127.0.0.1:6380`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/redis-fixture/start.sh
|
||||||
|
./scripts/redis-fixture/seed.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Stop and remove the fixture:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/redis-fixture/stop.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Seeded keys:
|
||||||
|
|
||||||
|
- `smoke:string`
|
||||||
|
- `smoke:string:ttl`
|
||||||
|
- `smoke:hash`
|
||||||
|
- `smoke:list`
|
||||||
|
- `smoke:set`
|
||||||
|
- `smoke:zset`
|
||||||
|
- `smoke:stream`
|
||||||
|
|
||||||
|
## Linux Smoke Steps
|
||||||
|
|
||||||
|
1. Run `./scripts/redis-fixture/start.sh`.
|
||||||
|
2. Run `./scripts/redis-fixture/seed.sh`.
|
||||||
|
3. Launch `pnpm run desktop:tauri:dev`.
|
||||||
|
4. In the desktop app, connect to host `127.0.0.1`, port `6380`, database `0`, no password, TLS disabled.
|
||||||
|
5. Verify the desktop window opens, app version plus runtime mode are visible in the header, and the connection test succeeds.
|
||||||
|
6. Verify the command console can run a safe read, such as `PING` or `GET smoke:string`.
|
||||||
|
7. Verify the seeded keys appear with expected types and TTL state in the browser and inspector surfaces.
|
||||||
|
8. Change TTL on `smoke:string:ttl`, remove TTL from the same key, and confirm the updated TTL state is reflected in the inspector.
|
||||||
|
9. Capture screenshots or terminal output for build, launch, connection success, key browse, TTL update, and command execution. If screenshots are unavailable, copy the in-app QA snapshot text into the smoke log.
|
||||||
|
10. Run `./scripts/redis-fixture/stop.sh` after the session.
|
||||||
|
|
||||||
|
## Current Validation Boundary
|
||||||
|
|
||||||
|
- `cargo test -p redis-core` passes in the current workspace.
|
||||||
|
- `pnpm run desktop:build` passes in the current workspace.
|
||||||
|
- `./scripts/redis-fixture/start.sh`, `./scripts/redis-fixture/seed.sh`, and `./scripts/redis-fixture/stop.sh` pass in the current workspace.
|
||||||
|
- `pnpm run desktop:tauri:build` now produces Linux `.deb` and `.rpm` artifacts under `target/release/bundle/`.
|
||||||
|
- The same Tauri build still fails in its AppImage stage with `io: Connection reset by peer (os error 104)`, so AppImage should be treated as not yet verified.
|
||||||
35
docs/qa/release-readiness-checklist.md
Normal file
35
docs/qa/release-readiness-checklist.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Release Readiness Checklist
|
||||||
|
|
||||||
|
Date: 2026-03-27
|
||||||
|
Owner: QA Engineer
|
||||||
|
|
||||||
|
## Must Be True Before Redis GUI V1 Can Ship
|
||||||
|
|
||||||
|
- A documented build or package path exists for Linux, macOS, and Windows.
|
||||||
|
- Smoke steps are executable and repeatable by someone other than the original implementer.
|
||||||
|
- Seed data or demo environment is documented and reproducible.
|
||||||
|
- Core workflows have explicit pass evidence:
|
||||||
|
- connection management
|
||||||
|
- key browsing
|
||||||
|
- search
|
||||||
|
- value editing
|
||||||
|
- TTL operations
|
||||||
|
- command console
|
||||||
|
- dangerous operation confirmation
|
||||||
|
- Known release blockers are triaged with owner and severity.
|
||||||
|
- Installer or portable package basic install, launch, and uninstall checks are complete.
|
||||||
|
|
||||||
|
## Current Release Blockers On 2026-03-27
|
||||||
|
|
||||||
|
- Blocked: Linux package artifact evidence now exists for `.deb` and `.rpm`, but AppImage is still unverified and there is no validated artifact evidence yet for macOS or Windows.
|
||||||
|
- Blocked: a repeatable Docker-backed seed and smoke path now exists, but no completed smoke evidence set has been captured from it yet.
|
||||||
|
- Blocked: release artifact naming now exists in produced Linux packages, but install and uninstall verification is still not documented with saved evidence.
|
||||||
|
- Blocked: the desktop app is not yet validated end-to-end against a seeded Redis path; browse, edit, and TTL controls are inspectable, but broader workflow evidence remains incomplete.
|
||||||
|
- Blocked: desktop runtime smoke evidence inside the real Tauri window is still pending even though current `redis-core` Rust test evidence and Linux package artifacts are reproducible from the present tree.
|
||||||
|
|
||||||
|
## Minimum Sign-Off Evidence
|
||||||
|
|
||||||
|
- Test run log with date, platform, build id, and tester.
|
||||||
|
- Screenshots or terminal evidence for each smoke step.
|
||||||
|
- Defect list with severity and explicit disposition.
|
||||||
|
- Final QA judgment: pass, fail, blocked, or pass with waiver.
|
||||||
46
docs/qa/test-strategy.md
Normal file
46
docs/qa/test-strategy.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# QA Test Strategy
|
||||||
|
|
||||||
|
Date: 2026-03-27
|
||||||
|
Owner: QA Engineer
|
||||||
|
|
||||||
|
## Test Objectives
|
||||||
|
|
||||||
|
- Validate Redis GUI V1 against explicit acceptance criteria, not against "it runs on my machine".
|
||||||
|
- Cover both happy paths and failure paths for connection, browsing, editing, TTL, console, and destructive actions.
|
||||||
|
- Keep release blocking conditions visible before packaging and launch week.
|
||||||
|
|
||||||
|
## Test Layers
|
||||||
|
|
||||||
|
| Layer | Purpose | Expected Automation | Exit Signal |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Unit | Validate pure logic, parsing, serialization, and state transitions | High | Deterministic CI pass |
|
||||||
|
| Integration | Validate Redis protocol handling, repository layer, seed setup, and persistence boundaries | Medium | Seeded Redis scenarios pass |
|
||||||
|
| UI smoke | Validate launch, connect, browse, edit, TTL, console, dangerous-action confirmation | Medium | One repeatable smoke flow per OS passes |
|
||||||
|
| Regression | Validate previously broken paths and high-risk areas before release | Mixed | No open critical regressions |
|
||||||
|
| Release readiness | Validate artifact, install, uninstall, and basic recovery behavior | Low to medium | Checklist complete with evidence |
|
||||||
|
|
||||||
|
## Environments
|
||||||
|
|
||||||
|
- Linux: current local environment available to QA.
|
||||||
|
- macOS: required for release acceptance, currently not provisioned in this repository.
|
||||||
|
- Windows: required for release acceptance, currently not provisioned in this repository.
|
||||||
|
|
||||||
|
## Test Data Strategy
|
||||||
|
|
||||||
|
- Maintain one documented seed dataset for predictable keys, TTL, and value shapes.
|
||||||
|
- Track Redis version used for QA evidence.
|
||||||
|
- Ensure cleanup steps return the environment to a known state.
|
||||||
|
|
||||||
|
## Evidence Standard
|
||||||
|
|
||||||
|
- Every pass or fail must cite the command, artifact, or screenshot used.
|
||||||
|
- "Blocked" is valid only when the missing prerequisite is named.
|
||||||
|
- "Not run" is distinct from "Pass" and must remain visible in checklists.
|
||||||
|
|
||||||
|
## Current Gaps On 2026-03-27
|
||||||
|
|
||||||
|
- Product shell exists as a visible desktop workspace baseline with shell-level validation and empty-state handling, but not as a complete Redis feature surface.
|
||||||
|
- No seed dataset or local Redis automation.
|
||||||
|
- No smoke harness, installer path, or CI workflow.
|
||||||
|
- Tauri packaging evidence is not currently present in the repository.
|
||||||
|
- Backend automated coverage is limited to foundation-model tests in `crates/redis-core`.
|
||||||
14
package.json
Normal file
14
package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "redis-gui-foundation",
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "pnpm@10.33.0",
|
||||||
|
"workspaces": [
|
||||||
|
"apps/desktop"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"desktop:dev": "pnpm --filter @redis-gui/desktop dev",
|
||||||
|
"desktop:build": "pnpm --filter @redis-gui/desktop build",
|
||||||
|
"desktop:tauri:dev": "pnpm --filter @redis-gui/desktop tauri dev",
|
||||||
|
"desktop:tauri:build": "pnpm --filter @redis-gui/desktop tauri build"
|
||||||
|
}
|
||||||
|
}
|
||||||
28
plans/2026-03-27-desktop-live-bridge.md
Normal file
28
plans/2026-03-27-desktop-live-bridge.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Desktop Live Bridge Plan
|
||||||
|
|
||||||
|
Date: 2026-03-27
|
||||||
|
Owner: Senior Frontend Engineer
|
||||||
|
Related issues: HAC-9, HAC-11
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Move the visible desktop shell from pure mock interaction to partial real integration by consuming the existing Tauri connection-test and command-execution commands without changing backend scope.
|
||||||
|
|
||||||
|
## Scope Delivered
|
||||||
|
|
||||||
|
1. Wire the connection test action to `test_redis_connection` in Tauri desktop mode.
|
||||||
|
2. Wire the command tray to `execute_redis_command` in Tauri desktop mode.
|
||||||
|
3. Keep browser-only `npm run desktop:dev` explicit about demo fallback so PM, CTO, and QA do not mistake it for live Redis behavior.
|
||||||
|
4. Update shared docs so run paths and acceptance language distinguish shell review from desktop integration review.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Do not add or alter Rust command contracts.
|
||||||
|
- Do not present browser-shell fallback as live Redis evidence.
|
||||||
|
- Keep key browsing, editing, and destructive actions inside current shell scope until backend feature contracts are ready.
|
||||||
|
|
||||||
|
## Exit Criteria
|
||||||
|
|
||||||
|
- `npm run desktop:build` succeeds after the bridge work.
|
||||||
|
- The UI labels command history as live or demo.
|
||||||
|
- Tauri desktop mode can attempt a real connection test and command execution through existing commands.
|
||||||
28
plans/2026-03-27-frontend-ui-foundation.md
Normal file
28
plans/2026-03-27-frontend-ui-foundation.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Frontend UI Foundation Plan
|
||||||
|
|
||||||
|
Date: 2026-03-27
|
||||||
|
Owner: Senior Frontend Engineer
|
||||||
|
Related issues: HAC-8, HAC-9, HAC-10, HAC-11
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Move `apps/desktop` from a placeholder shell to a visible React workspace that downstream product, backend, and QA work can inspect.
|
||||||
|
|
||||||
|
## Scope Delivered
|
||||||
|
|
||||||
|
1. React and Tailwind baseline inside `apps/desktop`.
|
||||||
|
2. Visible desktop workspace with connection library, DB switcher, key browser, inspector, and command tray.
|
||||||
|
3. Mock interaction for string edits, command history, and destructive confirmation.
|
||||||
|
4. Shared UI baseline and QA-facing acceptance notes in project docs.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Do not expand backend contract beyond the existing `backend_bootstrap` surface.
|
||||||
|
- Do not imply real Redis persistence; mock interactions must stay clearly marked.
|
||||||
|
- Keep the UI shaped for long-session operator work rather than generic dashboard styling.
|
||||||
|
|
||||||
|
## Exit Criteria
|
||||||
|
|
||||||
|
- `npm run desktop:build` succeeds.
|
||||||
|
- The shell is visually inspectable by PM, CTO, and QA.
|
||||||
|
- Shared docs explain scope, run path, and acceptance checks.
|
||||||
38
plans/2026-03-27-hac-15-desktop-build-smoke.md
Normal file
38
plans/2026-03-27-hac-15-desktop-build-smoke.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# HAC-15 Desktop Build, Packaging, And Smoke Plan
|
||||||
|
|
||||||
|
Date: 2026-03-27 UTC
|
||||||
|
Owner: Senior Backend Engineer
|
||||||
|
Issue: HAC-15
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Make the desktop repository usable through one stable build path, one stable package path, and one repeatable local Redis smoke path.
|
||||||
|
|
||||||
|
## Current Findings
|
||||||
|
|
||||||
|
- The original `tauri.conf.json` pre-build and pre-dev commands assumed the wrong working directory and caused `tauri build` to fail before the real Rust/Tauri flow began.
|
||||||
|
- The repo already had a valid desktop frontend build and a validated shared Rust backend core, but no stable `pnpm` workspace setup for the desktop shell.
|
||||||
|
- QA had explicit blockers around missing seed data, missing Docker or smoke automation, and missing reusable build instructions.
|
||||||
|
|
||||||
|
## Changes In This Pass
|
||||||
|
|
||||||
|
1. Standardize the workspace on `pnpm` by adding `packageManager`, `pnpm-workspace.yaml`, and root desktop scripts.
|
||||||
|
2. Fix Tauri `beforeDevCommand` and `beforeBuildCommand` so they execute against the desktop workspace directly.
|
||||||
|
3. Add a Docker-backed Redis fixture with start, seed, and stop scripts.
|
||||||
|
4. Add a local desktop smoke runbook and refresh README and QA evidence docs around the new commands and fixture path.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Passed: `cargo test -p redis-core`
|
||||||
|
- Passed: `pnpm install --lockfile-only`
|
||||||
|
- Passed: `pnpm run desktop:build`
|
||||||
|
- Passed: `./scripts/redis-fixture/start.sh`
|
||||||
|
- Passed: `./scripts/redis-fixture/seed.sh`
|
||||||
|
- Passed: `./scripts/redis-fixture/stop.sh`
|
||||||
|
- In progress: `pnpm run desktop:tauri:build` now clears the pre-build script failure and reaches Cargo dependency download and compile steps
|
||||||
|
|
||||||
|
## Remaining Risk
|
||||||
|
|
||||||
|
- This heartbeat has not yet produced a saved unsigned package artifact for Linux, macOS, or Windows.
|
||||||
|
- First-run Cargo package downloads remain slow and can serialize on the shared package-cache lock, so package verification should be rerun in a clean single-build window.
|
||||||
|
- End-to-end smoke evidence inside the desktop UI still depends on frontend surfaces that are only partially implemented.
|
||||||
36
plans/2026-03-27-qa-acceptance-baseline.md
Normal file
36
plans/2026-03-27-qa-acceptance-baseline.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# QA Acceptance Baseline Plan
|
||||||
|
|
||||||
|
Date: 2026-03-27
|
||||||
|
Owner: QA Engineer
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Create the first acceptance matrix and cross-platform smoke plan for Redis GUI V1, based only on repository evidence.
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
- `docs/qa/acceptance-matrix.md`
|
||||||
|
- `docs/qa/cross-platform-smoke-plan.md`
|
||||||
|
- `docs/qa/test-strategy.md`
|
||||||
|
- `docs/qa/defect-severity.md`
|
||||||
|
- `docs/qa/release-readiness-checklist.md`
|
||||||
|
|
||||||
|
## Immediate QA Priorities
|
||||||
|
|
||||||
|
1. Keep blocked acceptance items visible until product entrypoints exist.
|
||||||
|
2. Require a reproducible seed or demo path before any feature is marked accepted.
|
||||||
|
3. Require at least one smoke run per supported OS before release sign-off.
|
||||||
|
|
||||||
|
## Known Blockers
|
||||||
|
|
||||||
|
- `npm run desktop:tauri:build` currently fails because its pre-build command targets a non-existent parent `build` script.
|
||||||
|
- Repository does not yet contain Docker, demo, or seed assets.
|
||||||
|
- Current automated coverage is limited to foundation-model tests in `crates/redis-core`.
|
||||||
|
- Completed issue comments for `HAC-2` and `HAC-8` reference shared files that are absent from the current workspace, so QA cannot inspect those artifacts locally.
|
||||||
|
- Existing Rust test artifacts in `target/` are stale relative to current source, so backend baseline verification needs a fresh `cargo test` rerun.
|
||||||
|
|
||||||
|
## Alignment
|
||||||
|
|
||||||
|
- Product acceptance source: `HAC-2` issue scope and completion comment remain the current acceptance baseline for connect, browse, search, edit, and command console flows.
|
||||||
|
- CTO priority source: `HAC-15` is now the high-priority owner for Tauri build, packaging, local smoke steps, and Redis fixture conventions.
|
||||||
|
- QA closure for this plan remains downstream of `HAC-15`, backend feature issues `HAC-5` to `HAC-7`, and frontend feature issues `HAC-9` to `HAC-11`.
|
||||||
153
plans/2026-03-27-redis-gui-v1-status.md
Normal file
153
plans/2026-03-27-redis-gui-v1-status.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# Redis GUI V1 Status
|
||||||
|
|
||||||
|
Date: 2026-03-27 UTC
|
||||||
|
Project: `redis-gui-foundation`
|
||||||
|
Scope owner: Project Manager
|
||||||
|
|
||||||
|
Note:
|
||||||
|
- Frontend stack-baseline observations in this snapshot are superseded by `plans/2026-03-27-frontend-ui-foundation.md` and `docs/frontend-workspace-baseline.md`.
|
||||||
|
|
||||||
|
## Snapshot
|
||||||
|
|
||||||
|
- Project and goal are still marked `planned` in Paperclip, but execution has already started across product, architecture, backend, frontend, QA, and PM tracks.
|
||||||
|
- The repository contains a visible foundation: Rust workspace, `apps/desktop` Tauri shell, `crates/redis-core`, and `docs/architecture.md`.
|
||||||
|
- No issue is currently marked `blocked`, but the critical path is still concentrated in a few serial owner chains.
|
||||||
|
- There is no git commit history yet. Current project state exists only as an uncommitted workspace snapshot.
|
||||||
|
|
||||||
|
## Milestones
|
||||||
|
|
||||||
|
### 1. Foundation
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
- Product scope is inspectable.
|
||||||
|
- Architecture boundary is inspectable.
|
||||||
|
- Repo bootstrap is usable for follow-on implementation.
|
||||||
|
|
||||||
|
Issue alignment:
|
||||||
|
- Done: `HAC-2` Product scope definition
|
||||||
|
- In progress: `HAC-3` Architecture plan
|
||||||
|
- In progress: `HAC-4` Repo and Tauri/Rust foundation
|
||||||
|
- In progress: `HAC-13` Milestones, dependency map, risk tracking
|
||||||
|
|
||||||
|
Assessment:
|
||||||
|
- Foundation has visible repo evidence for architecture and bootstrap.
|
||||||
|
- Product scope has status evidence in Paperclip, but its comment points to files that are not present in the current workspace. This is an evidence gap that should be resolved before downstream teams rely on that artifact as the single source of truth.
|
||||||
|
|
||||||
|
### 2. Desktop
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
- Connection management, key browsing, value editing, and command console have an agreed UI baseline and working implementation path.
|
||||||
|
|
||||||
|
Issue alignment:
|
||||||
|
- In progress: `HAC-8` Desktop information architecture and visual baseline
|
||||||
|
- Todo: `HAC-9` Desktop shell and connection management UI
|
||||||
|
- Todo: `HAC-10` Key browser, viewer, editor
|
||||||
|
- Todo: `HAC-11` Command console and result panel
|
||||||
|
|
||||||
|
Assessment:
|
||||||
|
- Desktop execution has started only at the design baseline layer.
|
||||||
|
- The current repository still uses a minimal Vite shell with plain JS/CSS. The target stack in the project goal is React + shadcn/ui + Tailwind, so the desktop milestone is not yet on a stable implementation baseline.
|
||||||
|
|
||||||
|
### 3. Release
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
- QA matrix exists.
|
||||||
|
- Cross-platform smoke path is defined and executable.
|
||||||
|
- Desktop packaging path is proven on target operating systems.
|
||||||
|
|
||||||
|
Issue alignment:
|
||||||
|
- In progress: `HAC-12` Acceptance matrix and cross-platform smoke plan
|
||||||
|
|
||||||
|
Assessment:
|
||||||
|
- QA can define matrix structure now, but release readiness remains downstream of architecture, backend implementation, desktop implementation, and packaging verification.
|
||||||
|
|
||||||
|
## Issue Board By State
|
||||||
|
|
||||||
|
### Completed
|
||||||
|
|
||||||
|
- `HAC-2` Define Redis GUI V1 user scenarios, scope, and acceptance standards
|
||||||
|
|
||||||
|
### In Progress
|
||||||
|
|
||||||
|
- `HAC-3` Produce Tauri + Rust + React architecture
|
||||||
|
- `HAC-4` Initialize repo and Tauri / Rust foundation
|
||||||
|
- `HAC-8` Define desktop information architecture and visual baseline
|
||||||
|
- `HAC-12` Build QA matrix and smoke plan
|
||||||
|
- `HAC-13` Build milestones, dependency map, and risk tracking
|
||||||
|
|
||||||
|
### Not Started
|
||||||
|
|
||||||
|
- `HAC-5` Implement connection management and base command execution
|
||||||
|
- `HAC-6` Implement key browsing, search, and type detection
|
||||||
|
- `HAC-7` Implement typed value read/write abstraction
|
||||||
|
- `HAC-9` Implement desktop shell and connection management UI
|
||||||
|
- `HAC-10` Implement key browser, value viewer, and editor
|
||||||
|
- `HAC-11` Implement command console and result panel
|
||||||
|
|
||||||
|
### Blocked
|
||||||
|
|
||||||
|
- No issue is explicitly marked `blocked` as of 2026-03-27 UTC.
|
||||||
|
- Practical blockers still exist in the dependency chain and should be treated as pre-block conditions, not as proof of smooth execution.
|
||||||
|
|
||||||
|
## Dependency Map
|
||||||
|
|
||||||
|
- `HAC-2` -> `HAC-3`, `HAC-8`, `HAC-12`
|
||||||
|
- `HAC-3` -> `HAC-4`, `HAC-5`, `HAC-6`, `HAC-7`, `HAC-9`, `HAC-10`, `HAC-11`
|
||||||
|
- `HAC-4` -> `HAC-5`, `HAC-6`, `HAC-7`, `HAC-9`, `HAC-10`, `HAC-11`
|
||||||
|
- `HAC-8` -> `HAC-9`, `HAC-10`, `HAC-11`
|
||||||
|
- `HAC-5`, `HAC-6`, `HAC-7` -> `HAC-9`, `HAC-10`, `HAC-11`
|
||||||
|
- `HAC-9`, `HAC-10`, `HAC-11` -> `HAC-12`
|
||||||
|
|
||||||
|
## Current Critical Path
|
||||||
|
|
||||||
|
`HAC-2` -> `HAC-3` + `HAC-4` -> `HAC-5` + `HAC-6` + `HAC-7` and `HAC-8` -> `HAC-9` + `HAC-10` + `HAC-11` -> `HAC-12`
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- `HAC-4` has visible repo output already, but it is not yet marked done.
|
||||||
|
- The frontend implementation branch cannot move cleanly until `HAC-8` defines the intended interaction and visual contract.
|
||||||
|
- QA closure is downstream of both implementation branches, not parallel to them.
|
||||||
|
|
||||||
|
## CTO Attention Required
|
||||||
|
|
||||||
|
### 1. Product evidence gap
|
||||||
|
|
||||||
|
- `HAC-2` is marked done, but its completion comment references `docs/redis-gui-v1-product-requirements.md` and `plans/2026-03-27-redis-gui-foundation-product-plan.md`.
|
||||||
|
- Those files are not present in the current project workspace.
|
||||||
|
- Risk: downstream teams believe the product baseline is settled, but the local source of truth is missing.
|
||||||
|
|
||||||
|
### 2. Frontend baseline does not yet match target stack
|
||||||
|
|
||||||
|
- The goal and issue constraints specify React + shadcn/ui + Tailwind.
|
||||||
|
- The current desktop shell is plain Vite JS/CSS with no visible React or Tailwind setup.
|
||||||
|
- Risk: `HAC-9` to `HAC-11` may need stack migration work before feature delivery can become efficient.
|
||||||
|
|
||||||
|
### 3. Serial ownership on both critical branches
|
||||||
|
|
||||||
|
- One backend engineer owns `HAC-4` to `HAC-7`.
|
||||||
|
- One frontend engineer owns `HAC-8` to `HAC-11`.
|
||||||
|
- Risk: any stall in either owner chain becomes a full milestone delay because there is little parallel slack.
|
||||||
|
|
||||||
|
### 4. Release work started before packaging proof exists
|
||||||
|
|
||||||
|
- `HAC-12` is in progress, but there is no demonstrated cross-platform packaging result yet.
|
||||||
|
- Risk: the smoke plan may be structurally correct but still disconnected from the actual install/build path.
|
||||||
|
|
||||||
|
### 5. Project-level status drift
|
||||||
|
|
||||||
|
- Active execution is happening, but the project and goal still show `planned`.
|
||||||
|
- Risk: board-level reporting understates active work and can hide overdue coordination.
|
||||||
|
|
||||||
|
## Fixed Sync Rhythm
|
||||||
|
|
||||||
|
- Every owner must leave a comment when an issue moves from exploration to concrete output, from output to blocker, or from blocker to unblocked.
|
||||||
|
- PM heartbeat snapshot: 10:00 UTC and 18:00 UTC, or immediately when a critical-path issue changes to `blocked`.
|
||||||
|
- CTO escalation conditions:
|
||||||
|
- a critical-path issue has no new artifact or comment across one full sync window
|
||||||
|
- a done issue references missing artifacts
|
||||||
|
- frontend or backend baseline changes invalidate downstream queued work
|
||||||
|
|
||||||
|
## Immediate Follow-Up
|
||||||
|
|
||||||
|
- Confirm whether `HAC-2` deliverables exist outside the current workspace or need to be recreated here.
|
||||||
|
- Keep `HAC-3`, `HAC-4`, and `HAC-8` under tight observation because they define the usable baseline for all queued implementation work.
|
||||||
|
- Do not promote any milestone from foundation to desktop-complete until the repo contains inspectable artifacts for product, architecture, and frontend baseline together.
|
||||||
40
plans/2026-03-28-frontend-smoke-validation.md
Normal file
40
plans/2026-03-28-frontend-smoke-validation.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Frontend Smoke Validation Snapshot
|
||||||
|
|
||||||
|
Date: 2026-03-28 UTC
|
||||||
|
Owner: Senior Frontend Engineer
|
||||||
|
Related issues: HAC-9, HAC-11, HAC-15
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Revalidate the current desktop frontend and packaging path against the repository's shared smoke prerequisites.
|
||||||
|
|
||||||
|
## Evidence Captured
|
||||||
|
|
||||||
|
1. Passed: `cargo test -p redis-core`
|
||||||
|
2. Passed: `pnpm run desktop:build`
|
||||||
|
3. Passed: `./scripts/redis-fixture/start.sh`
|
||||||
|
4. Passed: `./scripts/redis-fixture/seed.sh`
|
||||||
|
5. Passed: `./scripts/redis-fixture/stop.sh`
|
||||||
|
6. Partial pass: `pnpm run desktop:tauri:build`
|
||||||
|
|
||||||
|
## Artifact Evidence
|
||||||
|
|
||||||
|
- `target/release/bundle/deb/Redis GUI Foundation_0.1.0_amd64.deb`
|
||||||
|
- `target/release/bundle/rpm/Redis GUI Foundation-0.1.0-1.x86_64.rpm`
|
||||||
|
|
||||||
|
## Packaging Outcome
|
||||||
|
|
||||||
|
- Tauri build completed the Linux release binary and produced `.deb` and `.rpm` artifacts.
|
||||||
|
- The same run failed in the AppImage step with `io: Connection reset by peer (os error 104)`.
|
||||||
|
|
||||||
|
## Remaining Gaps
|
||||||
|
|
||||||
|
- No saved Tauri-window screenshots or interaction logs exist yet for live browse, edit, TTL, or command flows.
|
||||||
|
- macOS and Windows package evidence remains absent.
|
||||||
|
- AppImage packaging needs a separate rerun or targeted investigation if that bundle format is required.
|
||||||
|
- This environment currently has no `DISPLAY`, `xvfb-run`, or desktop screenshot utility, so real-window visual evidence cannot be captured from this session alone.
|
||||||
|
- The desktop shell now includes a copyable QA snapshot so smoke context can still be recorded as text when screenshots are unavailable.
|
||||||
|
|
||||||
|
## Frontend Conclusion
|
||||||
|
|
||||||
|
The current repository is beyond shell-only validation: backend tests, browser build, Redis fixture bootstrapping, and Linux package generation all succeed in this environment. The next frontend validation step should happen in a launchable Tauri desktop session so QA can capture visual evidence for the live bridge workflows.
|
||||||
1993
pnpm-lock.yaml
generated
Normal file
1993
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
packages:
|
||||||
|
- apps/desktop
|
||||||
36
scripts/redis-fixture/seed.sh
Executable file
36
scripts/redis-fixture/seed.sh
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
container_name="${REDIS_FIXTURE_CONTAINER:-redis-gui-smoke}"
|
||||||
|
ttl_ms="${REDIS_FIXTURE_TTL_MS:-300000}"
|
||||||
|
|
||||||
|
if ! docker container inspect "$container_name" >/dev/null 2>&1; then
|
||||||
|
echo "Redis fixture container not found: $container_name" >&2
|
||||||
|
echo "Run scripts/redis-fixture/start.sh first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker exec "$container_name" redis-cli FLUSHALL >/dev/null
|
||||||
|
|
||||||
|
docker exec "$container_name" redis-cli SET smoke:string "hello from redis gui foundation" >/dev/null
|
||||||
|
docker exec "$container_name" redis-cli SET smoke:string:ttl "expires soon" >/dev/null
|
||||||
|
docker exec "$container_name" redis-cli PEXPIRE smoke:string:ttl "$ttl_ms" >/dev/null
|
||||||
|
docker exec "$container_name" redis-cli HSET smoke:hash field_a alpha field_b beta >/dev/null
|
||||||
|
docker exec "$container_name" redis-cli RPUSH smoke:list red green blue >/dev/null
|
||||||
|
docker exec "$container_name" redis-cli SADD smoke:set spring summer autumn >/dev/null
|
||||||
|
docker exec "$container_name" redis-cli ZADD smoke:zset 10 bronze 20 silver 30 gold >/dev/null
|
||||||
|
docker exec "$container_name" redis-cli XADD smoke:stream '*' level info message seeded >/dev/null
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
Seeded Redis fixture: $container_name
|
||||||
|
Target: 127.0.0.1:${REDIS_FIXTURE_PORT:-6380}
|
||||||
|
Database: 0
|
||||||
|
Keys:
|
||||||
|
- smoke:string
|
||||||
|
- smoke:string:ttl
|
||||||
|
- smoke:hash
|
||||||
|
- smoke:list
|
||||||
|
- smoke:set
|
||||||
|
- smoke:zset
|
||||||
|
- smoke:stream
|
||||||
|
EOF
|
||||||
35
scripts/redis-fixture/start.sh
Executable file
35
scripts/redis-fixture/start.sh
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
container_name="${REDIS_FIXTURE_CONTAINER:-redis-gui-smoke}"
|
||||||
|
host_port="${REDIS_FIXTURE_PORT:-6380}"
|
||||||
|
image="${REDIS_FIXTURE_IMAGE:-redis:7.4-alpine}"
|
||||||
|
|
||||||
|
if docker container inspect "$container_name" >/dev/null 2>&1; then
|
||||||
|
running="$(docker inspect -f '{{.State.Running}}' "$container_name")"
|
||||||
|
if [ "$running" = "true" ]; then
|
||||||
|
echo "Redis fixture already running: $container_name on 127.0.0.1:$host_port"
|
||||||
|
else
|
||||||
|
docker start "$container_name" >/dev/null
|
||||||
|
echo "Redis fixture started: $container_name on 127.0.0.1:$host_port"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
docker run -d \
|
||||||
|
--name "$container_name" \
|
||||||
|
-p "${host_port}:6379" \
|
||||||
|
"$image" \
|
||||||
|
redis-server --save '' --appendonly no >/dev/null
|
||||||
|
echo "Redis fixture created: $container_name on 127.0.0.1:$host_port"
|
||||||
|
fi
|
||||||
|
|
||||||
|
attempt=0
|
||||||
|
until docker exec "$container_name" redis-cli ping 2>/dev/null | grep -q '^PONG$'; do
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
if [ "$attempt" -ge 20 ]; then
|
||||||
|
echo "Redis fixture did not become ready within 20 seconds." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Redis fixture is ready."
|
||||||
11
scripts/redis-fixture/stop.sh
Executable file
11
scripts/redis-fixture/stop.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
container_name="${REDIS_FIXTURE_CONTAINER:-redis-gui-smoke}"
|
||||||
|
|
||||||
|
if docker container inspect "$container_name" >/dev/null 2>&1; then
|
||||||
|
docker rm -f "$container_name" >/dev/null
|
||||||
|
echo "Removed Redis fixture container: $container_name"
|
||||||
|
else
|
||||||
|
echo "Redis fixture container does not exist: $container_name"
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user