feat(desktop): add redis gui foundation baseline

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Senior Frontend Engineer
2026-03-28 10:39:40 +00:00
commit 8d50243d36
56 changed files with 17423 additions and 0 deletions

12
apps/desktop/index.html Normal file
View 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
View 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
View File

@@ -0,0 +1,2 @@
/target

View 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 = [] }

View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View 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"
]
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"default":{"identifier":"default","description":"Default capability for the primary desktop window.","local":true,"windows":["main"],"permissions":["core:default"]}}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 B

View 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");
}

View 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

File diff suppressed because it is too large Load Diff

View 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 };

View 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 };

View 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,
};

View 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 };

View 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 };

View 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);
}

View 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
View 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>,
);

View 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),
},
});