mirror of
https://github.com/khairul169/garage-webui.git
synced 2025-04-28 14:59:31 +07:00
feat: add cluster & bucket management
This commit is contained in:
parent
b0e5d53ee0
commit
dfb4e30e23
3
backend/.env.example
Normal file
3
backend/.env.example
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# App
|
||||||
|
API_BASE_URL=http://localhost:3903
|
||||||
|
CONFIG_PATH=/app/garage/garage.toml
|
175
backend/.gitignore
vendored
Normal file
175
backend/.gitignore
vendored
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
npm-debug.log_
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Caches
|
||||||
|
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
|
||||||
|
pids
|
||||||
|
_.pid
|
||||||
|
_.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
15
backend/README.md
Normal file
15
backend/README.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# backend
|
||||||
|
|
||||||
|
To install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
To run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
This project was created using `bun init` in bun v1.1.18. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
83
backend/lib/api.ts
Normal file
83
backend/lib/api.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { config } from "./garage";
|
||||||
|
|
||||||
|
type FetchOptions = Omit<RequestInit, "headers" | "body"> & {
|
||||||
|
params?: Record<string, any>;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
body?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const adminPort = config?.admin?.api_bind_addr?.split(":").pop();
|
||||||
|
const adminAddr =
|
||||||
|
import.meta.env.API_BASE_URL ||
|
||||||
|
config?.rpc_public_addr?.split(":")[0] + ":" + adminPort ||
|
||||||
|
"";
|
||||||
|
|
||||||
|
export const API_BASE_URL =
|
||||||
|
!adminAddr.startsWith("http") && !adminAddr.startsWith("https")
|
||||||
|
? `http://${adminAddr}`
|
||||||
|
: adminAddr;
|
||||||
|
|
||||||
|
export const API_ADMIN_KEY =
|
||||||
|
import.meta.env.API_ADMIN_KEY || config?.admin?.admin_token;
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
async fetch<T = any>(url: string, options?: Partial<FetchOptions>) {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
Authorization: `Bearer ${API_ADMIN_KEY}`,
|
||||||
|
};
|
||||||
|
const _url = new URL(API_BASE_URL + url);
|
||||||
|
|
||||||
|
if (options?.params) {
|
||||||
|
Object.entries(options.params).forEach(([key, value]) => {
|
||||||
|
_url.searchParams.set(key, String(value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof options?.body === "object" &&
|
||||||
|
!(options.body instanceof FormData)
|
||||||
|
) {
|
||||||
|
options.body = JSON.stringify(options.body);
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(_url, {
|
||||||
|
...options,
|
||||||
|
headers: { ...headers, ...(options?.headers || {}) },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = new Error(res.statusText);
|
||||||
|
(err as any).status = res.status;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isJson = res.headers
|
||||||
|
.get("Content-Type")
|
||||||
|
?.includes("application/json");
|
||||||
|
|
||||||
|
if (isJson) {
|
||||||
|
const json = (await res.json()) as T;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await res.text();
|
||||||
|
return text as unknown as T;
|
||||||
|
},
|
||||||
|
|
||||||
|
async get<T = any>(url: string, options?: Partial<FetchOptions>) {
|
||||||
|
return this.fetch<T>(url, {
|
||||||
|
...options,
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async post<T = any>(url: string, options?: Partial<FetchOptions>) {
|
||||||
|
return this.fetch<T>(url, {
|
||||||
|
...options,
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default api;
|
4
backend/lib/garage.ts
Normal file
4
backend/lib/garage.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import type { Config } from "../types/garage";
|
||||||
|
import { readTomlFile } from "./utils";
|
||||||
|
|
||||||
|
export const config = readTomlFile<Config>(process.env.CONFIG_PATH);
|
35
backend/lib/proxy-api.ts
Normal file
35
backend/lib/proxy-api.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import type { Context } from "hono";
|
||||||
|
import { API_ADMIN_KEY, API_BASE_URL } from "./api";
|
||||||
|
|
||||||
|
export const proxyApi = async (c: Context) => {
|
||||||
|
const url = new URL(c.req.url);
|
||||||
|
const reqUrl = new URL(API_BASE_URL + url.pathname + url.search);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers = c.req.raw.headers;
|
||||||
|
let body: BodyInit | ReadableStream<Uint8Array> | null = c.req.raw.body;
|
||||||
|
|
||||||
|
headers.set("authorization", `Bearer ${API_ADMIN_KEY}`);
|
||||||
|
|
||||||
|
if (headers.get("content-type")?.includes("application/json")) {
|
||||||
|
const json = await c.req.json();
|
||||||
|
body = JSON.stringify(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(reqUrl, {
|
||||||
|
...c.req.raw,
|
||||||
|
method: c.req.method,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
} catch (err) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (err as Error)?.message || "Server error",
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
9
backend/lib/utils.ts
Normal file
9
backend/lib/utils.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import toml from "toml";
|
||||||
|
|
||||||
|
export const readTomlFile = <T = any>(path?: string | null) => {
|
||||||
|
if (!path || !fs.existsSync(path)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return toml.parse(fs.readFileSync(path, "utf8")) as T;
|
||||||
|
};
|
23
backend/main.ts
Normal file
23
backend/main.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import { logger } from "hono/logger";
|
||||||
|
import router from "./routes";
|
||||||
|
import { proxyApi } from "./lib/proxy-api";
|
||||||
|
|
||||||
|
const HOST = import.meta.env.HOST || "0.0.0.0";
|
||||||
|
const PORT = Number(import.meta.env.PORT) || 3909;
|
||||||
|
|
||||||
|
const app = new Hono();
|
||||||
|
|
||||||
|
app.use(logger());
|
||||||
|
|
||||||
|
// API router
|
||||||
|
app.route("/", router);
|
||||||
|
|
||||||
|
// Proxy to garage admin API
|
||||||
|
app.all("*", proxyApi);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fetch: app.fetch,
|
||||||
|
hostname: HOST,
|
||||||
|
port: PORT,
|
||||||
|
};
|
18
backend/package.json
Normal file
18
backend/package.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "backend",
|
||||||
|
"module": "main.ts",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun --watch main.ts"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"hono": "^4.5.5",
|
||||||
|
"toml": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
69
backend/pnpm-lock.yaml
generated
Normal file
69
backend/pnpm-lock.yaml
generated
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
dependencies:
|
||||||
|
hono:
|
||||||
|
specifier: ^4.5.5
|
||||||
|
version: 4.5.5
|
||||||
|
toml:
|
||||||
|
specifier: ^3.0.0
|
||||||
|
version: 3.0.0
|
||||||
|
devDependencies:
|
||||||
|
'@types/bun':
|
||||||
|
specifier: latest
|
||||||
|
version: 1.1.6
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
'@types/bun@1.1.6':
|
||||||
|
resolution: {integrity: sha512-uJgKjTdX0GkWEHZzQzFsJkWp5+43ZS7HC8sZPFnOwnSo1AsNl2q9o2bFeS23disNDqbggEgyFkKCHl/w8iZsMA==}
|
||||||
|
|
||||||
|
'@types/node@20.12.14':
|
||||||
|
resolution: {integrity: sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==}
|
||||||
|
|
||||||
|
'@types/ws@8.5.12':
|
||||||
|
resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==}
|
||||||
|
|
||||||
|
bun-types@1.1.17:
|
||||||
|
resolution: {integrity: sha512-Z4+OplcSd/YZq7ZsrfD00DKJeCwuNY96a1IDJyR73+cTBaFIS7SC6LhpY/W3AMEXO9iYq5NJ58WAwnwL1p5vKg==}
|
||||||
|
|
||||||
|
hono@4.5.5:
|
||||||
|
resolution: {integrity: sha512-fXBXHqaVfimWofbelLXci8pZyIwBMkDIwCa4OwZvK+xVbEyYLELVP4DfbGaj1aEM6ZY3hHgs4qLvCO2ChkhgQw==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
toml@3.0.0:
|
||||||
|
resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==}
|
||||||
|
|
||||||
|
undici-types@5.26.5:
|
||||||
|
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
'@types/bun@1.1.6':
|
||||||
|
dependencies:
|
||||||
|
bun-types: 1.1.17
|
||||||
|
|
||||||
|
'@types/node@20.12.14':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 5.26.5
|
||||||
|
|
||||||
|
'@types/ws@8.5.12':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.12.14
|
||||||
|
|
||||||
|
bun-types@1.1.17:
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.12.14
|
||||||
|
'@types/ws': 8.5.12
|
||||||
|
|
||||||
|
hono@4.5.5: {}
|
||||||
|
|
||||||
|
toml@3.0.0: {}
|
||||||
|
|
||||||
|
undici-types@5.26.5: {}
|
19
backend/routes/buckets.ts
Normal file
19
backend/routes/buckets.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import api from "../lib/api";
|
||||||
|
|
||||||
|
export const buckets = new Hono()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all buckets
|
||||||
|
*/
|
||||||
|
.get("/", async (c) => {
|
||||||
|
const data = await api.get("/v1/bucket?list");
|
||||||
|
|
||||||
|
const buckets = await Promise.all(
|
||||||
|
data.map(async (bucket: any) => {
|
||||||
|
return api.get("/v1/bucket", { params: { id: bucket.id } });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(buckets);
|
||||||
|
});
|
21
backend/routes/config.ts
Normal file
21
backend/routes/config.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import { config } from "../lib/garage";
|
||||||
|
|
||||||
|
export const configRoute = new Hono()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get garage config
|
||||||
|
*/
|
||||||
|
.get("/", async (c) => {
|
||||||
|
const data = {
|
||||||
|
...(config || {}),
|
||||||
|
rpc_secret: undefined,
|
||||||
|
admin: {
|
||||||
|
...(config?.admin || {}),
|
||||||
|
admin_token: undefined,
|
||||||
|
metrics_token: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return c.json(data);
|
||||||
|
});
|
10
backend/routes/index.ts
Normal file
10
backend/routes/index.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import { buckets } from "./buckets";
|
||||||
|
import { configRoute } from "./config";
|
||||||
|
|
||||||
|
const router = new Hono()
|
||||||
|
//
|
||||||
|
.route("/config", configRoute)
|
||||||
|
.route("/buckets", buckets);
|
||||||
|
|
||||||
|
export default router;
|
27
backend/tsconfig.json
Normal file
27
backend/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Enable latest features
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
}
|
||||||
|
}
|
32
backend/types/garage.ts
Normal file
32
backend/types/garage.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
export type Config = {
|
||||||
|
metadata_dir: string;
|
||||||
|
data_dir: string;
|
||||||
|
db_engine: string;
|
||||||
|
metadata_auto_snapshot_interval: string;
|
||||||
|
replication_factor: number;
|
||||||
|
compression_level: number;
|
||||||
|
rpc_bind_addr: string;
|
||||||
|
rpc_public_addr: string;
|
||||||
|
rpc_secret: string;
|
||||||
|
s3_api?: S3API;
|
||||||
|
s3_web?: S3Web;
|
||||||
|
admin?: Admin;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Admin = {
|
||||||
|
api_bind_addr: string;
|
||||||
|
admin_token: string;
|
||||||
|
metrics_token: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type S3API = {
|
||||||
|
s3_region: string;
|
||||||
|
api_bind_addr: string;
|
||||||
|
root_domain: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type S3Web = {
|
||||||
|
bind_addr: string;
|
||||||
|
root_domain: string;
|
||||||
|
index: string;
|
||||||
|
};
|
@ -1,8 +1,12 @@
|
|||||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||||
|
import { lazy } from "react";
|
||||||
import AuthLayout from "@/components/layouts/auth-layout";
|
import AuthLayout from "@/components/layouts/auth-layout";
|
||||||
import MainLayout from "@/components/layouts/main-layout";
|
import MainLayout from "@/components/layouts/main-layout";
|
||||||
import ClusterPage from "@/pages/cluster/page";
|
|
||||||
import HomePage from "@/pages/home/page";
|
const ClusterPage = lazy(() => import("@/pages/cluster/page"));
|
||||||
|
const HomePage = lazy(() => import("@/pages/home/page"));
|
||||||
|
const BucketsPage = lazy(() => import("@/pages/buckets/page"));
|
||||||
|
const ManageBucketPage = lazy(() => import("@/pages/buckets/manage/page"));
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -21,6 +25,13 @@ const router = createBrowserRouter([
|
|||||||
path: "cluster",
|
path: "cluster",
|
||||||
Component: ClusterPage,
|
Component: ClusterPage,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "buckets",
|
||||||
|
children: [
|
||||||
|
{ index: true, Component: BucketsPage },
|
||||||
|
{ path: ":id", Component: ManageBucketPage },
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -5,7 +5,13 @@
|
|||||||
@layer base {
|
@layer base {
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
@apply bg-base-200;
|
@apply bg-base-200 overflow-hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.container {
|
||||||
|
@apply max-w-5xl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
40
src/components/containers/sidebar.tsx
Normal file
40
src/components/containers/sidebar.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Cylinder, HardDrive, LayoutDashboard } from "lucide-react";
|
||||||
|
import { Menu } from "react-daisyui";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
const Sidebar = () => {
|
||||||
|
return (
|
||||||
|
<aside className="bg-base-100 border-r border-base-300/30 w-[220px] overflow-y-auto">
|
||||||
|
<div className="p-4">
|
||||||
|
<img
|
||||||
|
src="https://garagehq.deuxfleurs.fr/images/garage-logo.svg"
|
||||||
|
alt="logo"
|
||||||
|
className="w-full max-w-[100px] mx-auto"
|
||||||
|
/>
|
||||||
|
<p className="text-sm font-medium text-center">WebUI</p>
|
||||||
|
</div>
|
||||||
|
<Menu className="gap-y-1">
|
||||||
|
<Menu.Item>
|
||||||
|
<Link to="/">
|
||||||
|
<LayoutDashboard />
|
||||||
|
<p>Dashboard</p>
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<Link to="/cluster">
|
||||||
|
<HardDrive />
|
||||||
|
<p>Cluster</p>
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<Link to="/buckets">
|
||||||
|
<Cylinder />
|
||||||
|
<p>Buckets</p>
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sidebar;
|
64
src/components/containers/tab-view.tsx
Normal file
64
src/components/containers/tab-view.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { LucideIcon } from "lucide-react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { Tabs } from "react-daisyui";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
|
export type Tab = {
|
||||||
|
name: string;
|
||||||
|
title?: string;
|
||||||
|
icon?: LucideIcon;
|
||||||
|
Component?: () => JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
tabs: Tab[];
|
||||||
|
name?: string;
|
||||||
|
className?: string;
|
||||||
|
contentClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TabView = ({
|
||||||
|
tabs,
|
||||||
|
name = "tab",
|
||||||
|
className,
|
||||||
|
contentClassName,
|
||||||
|
}: Props) => {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const curTab = searchParams.get(name) || tabs[0].name;
|
||||||
|
|
||||||
|
const content = useMemo(() => {
|
||||||
|
const Comp = tabs.find((tab) => tab.name === curTab)?.Component;
|
||||||
|
return Comp ? <Comp /> : null;
|
||||||
|
}, [curTab, tabs]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tabs
|
||||||
|
variant="boxed"
|
||||||
|
className={cn("w-auto inline-flex flex-row items-stretch", className)}
|
||||||
|
>
|
||||||
|
{tabs.map(({ icon: Icon, ...tab }) => (
|
||||||
|
<Tabs.Tab
|
||||||
|
key={tab.name}
|
||||||
|
active={curTab === tab.name}
|
||||||
|
className="flex flex-row items-center gap-x-2 h-auto"
|
||||||
|
onClick={() => {
|
||||||
|
setSearchParams((params) => {
|
||||||
|
params.set(name, tab.name);
|
||||||
|
return params;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Icon ? <Icon size={20} /> : null}
|
||||||
|
<span>{tab.title || tab.name}</span>
|
||||||
|
</Tabs.Tab>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<div className={cn("mt-4", contentClassName)}>{content}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TabView;
|
@ -1,8 +1,9 @@
|
|||||||
import { PageContext } from "@/context/page-context";
|
import { PageContext } from "@/context/page-context";
|
||||||
import { useContext } from "react";
|
import { Suspense, useContext } from "react";
|
||||||
import { Link, Outlet } from "react-router-dom";
|
import { Outlet, useNavigate } from "react-router-dom";
|
||||||
import { Menu } from "react-daisyui";
|
import Sidebar from "../containers/sidebar";
|
||||||
import { HardDrive, LayoutDashboard } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
import Button from "../ui/button";
|
||||||
|
|
||||||
const MainLayout = () => {
|
const MainLayout = () => {
|
||||||
return (
|
return (
|
||||||
@ -13,50 +14,33 @@ const MainLayout = () => {
|
|||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<main className="flex-1 overflow-y-auto p-4 md:p-8">
|
<main className="flex-1 overflow-y-auto p-4 md:p-8">
|
||||||
<div className="container max-w-5xl">
|
<Suspense>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</Suspense>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Sidebar = () => {
|
|
||||||
return (
|
|
||||||
<aside className="bg-base-100 border-r border-base-300/30 w-[220px] overflow-y-auto">
|
|
||||||
<div className="p-4">
|
|
||||||
<img
|
|
||||||
src="https://garagehq.deuxfleurs.fr/images/garage-logo.svg"
|
|
||||||
alt="logo"
|
|
||||||
className="w-full max-w-[100px] mx-auto"
|
|
||||||
/>
|
|
||||||
<p className="text-sm font-medium text-center">WebUI</p>
|
|
||||||
</div>
|
|
||||||
<Menu className="gap-y-1">
|
|
||||||
<Menu.Item>
|
|
||||||
<Link to="/">
|
|
||||||
<LayoutDashboard />
|
|
||||||
<p>Dashboard</p>
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item>
|
|
||||||
<Link to="/cluster">
|
|
||||||
<HardDrive />
|
|
||||||
<p>Cluster</p>
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu>
|
|
||||||
</aside>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const page = useContext(PageContext);
|
const page = useContext(PageContext);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="bg-base-100 p-4 md:p-8 md:py-6 flex flex-row items-center gap-4">
|
<header className="bg-base-100 px-4 h-16 md:px-8 md:h-20 flex flex-row items-center gap-4">
|
||||||
<h1 className="text-2xl font-medium">{page?.title || "Dashboard"}</h1>
|
{page?.prev ? (
|
||||||
|
<Button
|
||||||
|
href={page.prev}
|
||||||
|
onClick={() => navigate(page.prev!, { replace: true })}
|
||||||
|
color="ghost"
|
||||||
|
shape="circle"
|
||||||
|
className="-ml-4"
|
||||||
|
>
|
||||||
|
<ArrowLeft />
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
<h1 className="text-xl">{page?.title || "Dashboard"}</h1>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
23
src/components/ui/button.tsx
Normal file
23
src/components/ui/button.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentPropsWithoutRef, forwardRef } from "react";
|
||||||
|
import { Button as BaseButton } from "react-daisyui";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
type ButtonProps = ComponentPropsWithoutRef<typeof BaseButton> & {
|
||||||
|
href?: string;
|
||||||
|
target?: "_blank" | "_self" | "_parent" | "_top";
|
||||||
|
};
|
||||||
|
|
||||||
|
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ href, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<BaseButton
|
||||||
|
ref={ref}
|
||||||
|
tag={href ? Link : undefined}
|
||||||
|
{...props}
|
||||||
|
{...(href ? { to: href } : {})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Button;
|
41
src/components/ui/chips.tsx
Normal file
41
src/components/ui/chips.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import React, { forwardRef } from "react";
|
||||||
|
import { Button } from "react-daisyui";
|
||||||
|
|
||||||
|
type Props = React.ComponentPropsWithoutRef<"div"> & {
|
||||||
|
onClick?: () => void;
|
||||||
|
onRemove?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Chips = forwardRef<HTMLDivElement, Props>(
|
||||||
|
({ className, children, onRemove, ...props }, ref) => {
|
||||||
|
const Comp = props.onClick ? "button" : "div";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
ref={ref as never}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex flex-row items-center h-8 px-4 rounded-full text-sm border border-primary/80 text-base-content cursor-default",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...(props as any)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{onRemove ? (
|
||||||
|
<Button
|
||||||
|
color="ghost"
|
||||||
|
shape="circle"
|
||||||
|
size="sm"
|
||||||
|
className="-mr-3"
|
||||||
|
onClick={onRemove}
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</Comp>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Chips;
|
@ -1,48 +1,59 @@
|
|||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
|
memo,
|
||||||
PropsWithChildren,
|
PropsWithChildren,
|
||||||
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
type PageContextValues = {
|
type PageContextValues = {
|
||||||
title?: string | null;
|
title: string | null;
|
||||||
setTitle: (title?: string | null) => void;
|
prev: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageContext = createContext<PageContextValues | null>(null);
|
export const PageContext = createContext<
|
||||||
|
| (PageContextValues & {
|
||||||
|
setValue: (values: Partial<PageContextValues>) => void;
|
||||||
|
})
|
||||||
|
| null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
const initialValues: PageContextValues = {
|
||||||
|
title: null,
|
||||||
|
prev: null,
|
||||||
|
};
|
||||||
|
|
||||||
export const PageContextProvider = ({ children }: PropsWithChildren) => {
|
export const PageContextProvider = ({ children }: PropsWithChildren) => {
|
||||||
const [title, setTitle] = useState<PageContextValues["title"]>(null);
|
const [values, setValues] = useState<PageContextValues>(initialValues);
|
||||||
|
|
||||||
const contextValues = {
|
const setValue = useCallback((value: Partial<PageContextValues>) => {
|
||||||
title,
|
setValues((prev) => ({ ...prev, ...value }));
|
||||||
setTitle,
|
}, []);
|
||||||
};
|
|
||||||
|
|
||||||
return <PageContext.Provider children={children} value={contextValues} />;
|
return (
|
||||||
|
<PageContext.Provider children={children} value={{ ...values, setValue }} />
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type PageProps = {
|
type PageProps = Partial<PageContextValues>;
|
||||||
title?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Page = ({ title }: PageProps) => {
|
const Page = memo((props: PageProps) => {
|
||||||
const context = useContext(PageContext);
|
const context = useContext(PageContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("Page component must be used within a PageContextProvider");
|
throw new Error("Page component must be used within a PageContextProvider");
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
context.setTitle(title);
|
context.setValue(props);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
context.setTitle(null);
|
context.setValue(initialValues);
|
||||||
};
|
};
|
||||||
}, [title, context.setTitle]);
|
}, [props, context.setValue]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
});
|
||||||
|
|
||||||
export default Page;
|
export default Page;
|
||||||
|
10
src/hooks/useConfig.ts
Normal file
10
src/hooks/useConfig.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import api from "@/lib/api";
|
||||||
|
import { Config } from "@/types/garage";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export const useConfig = () => {
|
||||||
|
return useQuery<Config>({
|
||||||
|
queryKey: ["config"],
|
||||||
|
queryFn: () => api.get("/config"),
|
||||||
|
});
|
||||||
|
};
|
20
src/hooks/useDebounce.ts
Normal file
20
src/hooks/useDebounce.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { useCallback, useRef } from "react";
|
||||||
|
|
||||||
|
export const useDebounce = <T extends (...args: any[]) => void>(
|
||||||
|
fn: T,
|
||||||
|
delay: number = 500
|
||||||
|
) => {
|
||||||
|
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
const debouncedFn = useCallback(
|
||||||
|
(...args: any[]) => {
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
}
|
||||||
|
timerRef.current = setTimeout(() => fn(...args), delay);
|
||||||
|
},
|
||||||
|
[fn]
|
||||||
|
);
|
||||||
|
|
||||||
|
return debouncedFn as T;
|
||||||
|
};
|
@ -4,8 +4,6 @@ type FetchOptions = Omit<RequestInit, "headers" | "body"> & {
|
|||||||
body?: any;
|
body?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ADMIN_KEY = "E1tDBf4mhc/XMHq1YJkDE6N1j3AZG9dRWR+vDDTyASk=";
|
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
async fetch<T = any>(url: string, options?: Partial<FetchOptions>) {
|
async fetch<T = any>(url: string, options?: Partial<FetchOptions>) {
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
@ -25,10 +23,6 @@ const api = {
|
|||||||
headers["Content-Type"] = "application/json";
|
headers["Content-Type"] = "application/json";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ADMIN_KEY) {
|
|
||||||
headers["Authorization"] = `Bearer ${ADMIN_KEY}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(_url, {
|
const res = await fetch(_url, {
|
||||||
...options,
|
...options,
|
||||||
headers: { ...headers, ...(options?.headers || {}) },
|
headers: { ...headers, ...(options?.headers || {}) },
|
||||||
@ -64,6 +58,20 @@ const api = {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async put<T = any>(url: string, options?: Partial<FetchOptions>) {
|
||||||
|
return this.fetch<T>(url, {
|
||||||
|
...options,
|
||||||
|
method: "PUT",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete<T = any>(url: string, options?: Partial<FetchOptions>) {
|
||||||
|
return this.fetch<T>(url, {
|
||||||
|
...options,
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
47
src/pages/buckets/components/bucket-card.tsx
Normal file
47
src/pages/buckets/components/bucket-card.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Bucket } from "../types";
|
||||||
|
import { ArchiveIcon, ChartPie, ChartScatter } from "lucide-react";
|
||||||
|
import { readableBytes } from "@/lib/utils";
|
||||||
|
import Button from "@/components/ui/button";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: Bucket;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BucketCard = ({ data }: Props) => {
|
||||||
|
return (
|
||||||
|
<div className="card card-body p-6">
|
||||||
|
<div className="flex flex-row items-start gap-4 p-2 pb-0">
|
||||||
|
<ArchiveIcon size={28} />
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-xl font-medium">
|
||||||
|
{data.globalAliases?.join(", ")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm flex items-center gap-1">
|
||||||
|
<ChartPie className="inline" size={16} />
|
||||||
|
Usage
|
||||||
|
</p>
|
||||||
|
<p className="text-2xl font-medium">{readableBytes(data.bytes)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm flex items-center gap-1">
|
||||||
|
<ChartScatter className="inline" size={16} />
|
||||||
|
Objects
|
||||||
|
</p>
|
||||||
|
<p className="text-2xl font-medium">{data.objects}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex flex-row justify-end gap-4">
|
||||||
|
<Button href={`/buckets/${data.id}`}>Manage</Button>
|
||||||
|
{/* <Button color="primary">Browse</Button> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BucketCard;
|
10
src/pages/buckets/hooks.ts
Normal file
10
src/pages/buckets/hooks.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import api from "@/lib/api";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { GetBucketRes } from "./types";
|
||||||
|
|
||||||
|
export const useBuckets = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["buckets"],
|
||||||
|
queryFn: () => api.get<GetBucketRes>("/buckets"),
|
||||||
|
});
|
||||||
|
};
|
32
src/pages/buckets/manage/components/overview-aliases.tsx
Normal file
32
src/pages/buckets/manage/components/overview-aliases.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Button } from "react-daisyui";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
import Chips from "@/components/ui/chips";
|
||||||
|
import { Bucket } from "../../types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: Bucket;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AliasesSection = ({ data }: Props) => {
|
||||||
|
const aliases = data?.globalAliases?.slice(1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="inline label label-text">Aliases</p>
|
||||||
|
|
||||||
|
<div className="flex flex-row flex-wrap gap-2 mt-1">
|
||||||
|
{aliases?.map((alias: string) => (
|
||||||
|
<Chips key={alias} onRemove={() => {}}>
|
||||||
|
{alias}
|
||||||
|
</Chips>
|
||||||
|
))}
|
||||||
|
<Button size="sm">
|
||||||
|
<Plus className="-ml-1" size={18} />
|
||||||
|
Add Alias
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AliasesSection;
|
97
src/pages/buckets/manage/components/overview-quota.tsx
Normal file
97
src/pages/buckets/manage/components/overview-quota.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { Controller, DeepPartial, useForm, useWatch } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { QuotaSchema, quotaSchema } from "../schema";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { Input, Toggle } from "react-daisyui";
|
||||||
|
import FormControl from "@/components/ui/form-control";
|
||||||
|
import { useDebounce } from "@/hooks/useDebounce";
|
||||||
|
import { useUpdateBucket } from "../hooks";
|
||||||
|
import { Bucket } from "../../types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: Bucket;
|
||||||
|
};
|
||||||
|
|
||||||
|
const QuotaSection = ({ data }: Props) => {
|
||||||
|
const form = useForm<QuotaSchema>({
|
||||||
|
resolver: zodResolver(quotaSchema),
|
||||||
|
});
|
||||||
|
const isEnabled = useWatch({ control: form.control, name: "enabled" });
|
||||||
|
|
||||||
|
const updateMutation = useUpdateBucket(data?.id);
|
||||||
|
|
||||||
|
const onChange = useDebounce((values: DeepPartial<QuotaSchema>) => {
|
||||||
|
const { enabled } = values;
|
||||||
|
const maxObjects = Number(values.maxObjects);
|
||||||
|
const maxSize = Math.round(Number(values.maxSize) * 1024 * 1024);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
maxObjects: enabled && maxObjects > 0 ? maxObjects : null,
|
||||||
|
maxSize: enabled && maxSize > 0 ? maxSize : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
updateMutation.mutate({ quotas: data });
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset({
|
||||||
|
enabled:
|
||||||
|
data?.quotas?.maxSize != null || data?.quotas?.maxObjects != null,
|
||||||
|
maxSize: data?.quotas?.maxSize
|
||||||
|
? data?.quotas?.maxSize / 1024 / 1024
|
||||||
|
: null,
|
||||||
|
maxObjects: data?.quotas?.maxObjects || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { unsubscribe } = form.watch((values) => onChange(values));
|
||||||
|
return unsubscribe;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-8">
|
||||||
|
<p className="label label-text py-0">Quotas</p>
|
||||||
|
|
||||||
|
<label className="inline-flex label label-text gap-2 cursor-pointer">
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="enabled"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Toggle {...(field as any)} checked={field.value} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
Enabled
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{isEnabled && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<FormControl
|
||||||
|
form={form}
|
||||||
|
name="maxObjects"
|
||||||
|
title="Max Objects"
|
||||||
|
render={(field) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
value={String(field.value || "")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
form={form}
|
||||||
|
name="maxSize"
|
||||||
|
title="Max Size (GB)"
|
||||||
|
render={(field) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
value={String(field.value || "")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QuotaSection;
|
51
src/pages/buckets/manage/components/overview-tab.tsx
Normal file
51
src/pages/buckets/manage/components/overview-tab.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { Card } from "react-daisyui";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { useBucket } from "../hooks";
|
||||||
|
import { ChartPie, ChartScatter } from "lucide-react";
|
||||||
|
import { readableBytes } from "@/lib/utils";
|
||||||
|
import WebsiteAccessSection from "./overview-website-access";
|
||||||
|
import AliasesSection from "./overview-aliases";
|
||||||
|
import QuotaSection from "./overview-quota";
|
||||||
|
|
||||||
|
const OverviewTab = () => {
|
||||||
|
const { id } = useParams();
|
||||||
|
const { data } = useBucket(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-8">
|
||||||
|
<Card className="card-body gap-0 items-start">
|
||||||
|
<Card.Title>Summary</Card.Title>
|
||||||
|
|
||||||
|
<AliasesSection data={data} />
|
||||||
|
<WebsiteAccessSection data={data} />
|
||||||
|
<QuotaSection data={data} />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="card-body">
|
||||||
|
<Card.Title>Usage</Card.Title>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4 mt-4">
|
||||||
|
<div className="flex flex-row gap-3">
|
||||||
|
<ChartPie className="mt-1" size={20} />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm flex items-center gap-1">Storage</p>
|
||||||
|
<p className="text-2xl font-medium">
|
||||||
|
{readableBytes(data?.bytes)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-3">
|
||||||
|
<ChartScatter className="mt-1" size={20} />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm flex items-center gap-1">Objects</p>
|
||||||
|
<p className="text-2xl font-medium">{data?.objects}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OverviewTab;
|
143
src/pages/buckets/manage/components/overview-website-access.tsx
Normal file
143
src/pages/buckets/manage/components/overview-website-access.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { Controller, DeepPartial, useForm, useWatch } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { websiteConfigSchema, WebsiteConfigSchema } from "../schema";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { Input, Toggle } from "react-daisyui";
|
||||||
|
import FormControl from "@/components/ui/form-control";
|
||||||
|
import { useDebounce } from "@/hooks/useDebounce";
|
||||||
|
import { useUpdateBucket } from "../hooks";
|
||||||
|
import { useConfig } from "@/hooks/useConfig";
|
||||||
|
import { Info, LinkIcon } from "lucide-react";
|
||||||
|
import Button from "@/components/ui/button";
|
||||||
|
import { Bucket } from "../../types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: Bucket;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WebsiteAccessSection = ({ data }: Props) => {
|
||||||
|
const { data: config } = useConfig();
|
||||||
|
const form = useForm<WebsiteConfigSchema>({
|
||||||
|
resolver: zodResolver(websiteConfigSchema),
|
||||||
|
});
|
||||||
|
const bucketName = data?.globalAliases[0] || "";
|
||||||
|
const isEnabled = useWatch({ control: form.control, name: "websiteAccess" });
|
||||||
|
|
||||||
|
const websitePort = config?.s3_web?.bind_addr?.split(":").pop() || "80";
|
||||||
|
const rootDomain = config?.s3_web?.root_domain;
|
||||||
|
|
||||||
|
const updateMutation = useUpdateBucket(data?.id);
|
||||||
|
|
||||||
|
const onChange = useDebounce((values: DeepPartial<WebsiteConfigSchema>) => {
|
||||||
|
const data = {
|
||||||
|
enabled: values.websiteAccess,
|
||||||
|
indexDocument: values.websiteAccess
|
||||||
|
? values.websiteConfig?.indexDocument
|
||||||
|
: undefined,
|
||||||
|
errorDocument: values.websiteAccess
|
||||||
|
? values.websiteConfig?.errorDocument
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
updateMutation.mutate({
|
||||||
|
websiteAccess: data,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset({
|
||||||
|
websiteAccess: data?.websiteAccess,
|
||||||
|
websiteConfig: {
|
||||||
|
indexDocument: data?.websiteConfig?.indexDocument || "index.html",
|
||||||
|
errorDocument: data?.websiteConfig?.errorDocument || "error/400.html",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { unsubscribe } = form.watch((values) => onChange(values));
|
||||||
|
return unsubscribe;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-8">
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<p className="label label-text py-0 grow-0">Website Access</p>
|
||||||
|
<Button
|
||||||
|
href="https://garagehq.deuxfleurs.fr/documentation/cookbook/exposing-websites"
|
||||||
|
target="_blank"
|
||||||
|
size="sm"
|
||||||
|
shape="circle"
|
||||||
|
color="ghost"
|
||||||
|
>
|
||||||
|
<Info size={16} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="inline-flex label label-text gap-2 cursor-pointer">
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="websiteAccess"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Toggle {...(field as any)} checked={field.value} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
Enabled
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{isEnabled && (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<FormControl
|
||||||
|
form={form}
|
||||||
|
name="websiteConfig.indexDocument"
|
||||||
|
title="Index Document"
|
||||||
|
render={(field) => (
|
||||||
|
<Input {...field} value={String(field.value || "")} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
form={form}
|
||||||
|
name="websiteConfig.errorDocument"
|
||||||
|
title="Error Document"
|
||||||
|
render={(field) => (
|
||||||
|
<Input {...field} value={String(field.value || "")} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 alert flex flex-row flex-wrap">
|
||||||
|
<a
|
||||||
|
href={`http://${bucketName}`}
|
||||||
|
className="inline-flex items-center flex-row gap-2 font-medium hover:link"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<LinkIcon size={14} />
|
||||||
|
{bucketName}
|
||||||
|
</a>
|
||||||
|
{rootDomain ? (
|
||||||
|
<>
|
||||||
|
<a
|
||||||
|
href={`http://${bucketName}${rootDomain}`}
|
||||||
|
className="inline-flex items-center flex-row gap-2 font-medium hover:link"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<LinkIcon size={14} />
|
||||||
|
{bucketName + rootDomain}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={`http://${bucketName}${rootDomain}:${websitePort}`}
|
||||||
|
className="inline-flex items-center flex-row gap-2 font-medium hover:link"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<LinkIcon size={14} />
|
||||||
|
{bucketName + rootDomain + ":" + websitePort}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WebsiteAccessSection;
|
19
src/pages/buckets/manage/hooks.ts
Normal file
19
src/pages/buckets/manage/hooks.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import api from "@/lib/api";
|
||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import { Bucket } from "../types";
|
||||||
|
|
||||||
|
export const useBucket = (id?: string | null) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["bucket", id],
|
||||||
|
queryFn: () => api.get<Bucket>("/v1/bucket", { params: { id } }),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateBucket = (id?: string | null) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (values: any) => {
|
||||||
|
return api.put<any>("/v1/bucket", { params: { id }, body: values });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
36
src/pages/buckets/manage/page.tsx
Normal file
36
src/pages/buckets/manage/page.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { useBucket } from "./hooks";
|
||||||
|
import Page from "@/context/page-context";
|
||||||
|
import TabView, { Tab } from "@/components/containers/tab-view";
|
||||||
|
import { ChartLine, FolderSearch } from "lucide-react";
|
||||||
|
import OverviewTab from "./components/overview-tab";
|
||||||
|
|
||||||
|
const tabs: Tab[] = [
|
||||||
|
{
|
||||||
|
name: "overview",
|
||||||
|
title: "Overview",
|
||||||
|
icon: ChartLine,
|
||||||
|
Component: OverviewTab,
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// name: "browse",
|
||||||
|
// title: "Browse",
|
||||||
|
// icon: FolderSearch,
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
|
||||||
|
const ManageBucketPage = () => {
|
||||||
|
const { id } = useParams();
|
||||||
|
const { data } = useBucket(id);
|
||||||
|
|
||||||
|
const name = data?.globalAliases[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<Page title={name || "Manage Bucket"} prev="/buckets" />
|
||||||
|
<TabView tabs={tabs} className="bg-base-100 h-14 px-1.5" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ManageBucketPage;
|
18
src/pages/buckets/manage/schema.ts
Normal file
18
src/pages/buckets/manage/schema.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const websiteConfigSchema = z.object({
|
||||||
|
websiteAccess: z.boolean(),
|
||||||
|
websiteConfig: z
|
||||||
|
.object({ indexDocument: z.string(), errorDocument: z.string() })
|
||||||
|
.nullish(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type WebsiteConfigSchema = z.infer<typeof websiteConfigSchema>;
|
||||||
|
|
||||||
|
export const quotaSchema = z.object({
|
||||||
|
enabled: z.boolean(),
|
||||||
|
maxObjects: z.coerce.number().nullish(),
|
||||||
|
maxSize: z.coerce.number().nullish(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type QuotaSchema = z.infer<typeof quotaSchema>;
|
34
src/pages/buckets/page.tsx
Normal file
34
src/pages/buckets/page.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import Page from "@/context/page-context";
|
||||||
|
import { useBuckets } from "./hooks";
|
||||||
|
import { Button, Input } from "react-daisyui";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
import BucketCard from "./components/bucket-card";
|
||||||
|
|
||||||
|
const BucketsPage = () => {
|
||||||
|
const { data } = useBuckets();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<Page title="Buckets" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<Input placeholder="Search..." />
|
||||||
|
<div className="flex-1" />
|
||||||
|
<Button color="primary">
|
||||||
|
<Plus />
|
||||||
|
Create Bucket
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-8 items-stretch mt-4 md:mt-8">
|
||||||
|
{data?.map((bucket) => (
|
||||||
|
<BucketCard key={bucket.id} data={bucket} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BucketsPage;
|
41
src/pages/buckets/types.ts
Normal file
41
src/pages/buckets/types.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
|
||||||
|
export type GetBucketRes = Bucket[];
|
||||||
|
|
||||||
|
export type Bucket = {
|
||||||
|
id: string;
|
||||||
|
globalAliases: string[];
|
||||||
|
websiteAccess: boolean;
|
||||||
|
websiteConfig?: WebsiteConfig | null;
|
||||||
|
keys: Key[];
|
||||||
|
objects: number;
|
||||||
|
bytes: number;
|
||||||
|
unfinishedUploads: number;
|
||||||
|
unfinishedMultipartUploads: number;
|
||||||
|
unfinishedMultipartUploadParts: number;
|
||||||
|
unfinishedMultipartUploadBytes: number;
|
||||||
|
quotas: Quotas;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Key = {
|
||||||
|
accessKeyId: string;
|
||||||
|
name: string;
|
||||||
|
permissions: Permissions;
|
||||||
|
bucketLocalAliases: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Permissions = {
|
||||||
|
read: boolean;
|
||||||
|
write: boolean;
|
||||||
|
owner: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WebsiteConfig = {
|
||||||
|
indexDocument: string;
|
||||||
|
errorDocument: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Quotas = {
|
||||||
|
maxSize: null;
|
||||||
|
maxObjects: null;
|
||||||
|
};
|
@ -7,6 +7,7 @@ import { ConnectNodeSchema, connectNodeSchema } from "../schema";
|
|||||||
import { useCallback, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { Plug } from "lucide-react";
|
||||||
|
|
||||||
const ConnectNodeDialog = () => {
|
const ConnectNodeDialog = () => {
|
||||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||||
@ -45,6 +46,7 @@ const ConnectNodeDialog = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button color="primary" onClick={handleShow}>
|
<Button color="primary" onClick={handleShow}>
|
||||||
|
<Plug />
|
||||||
Connect
|
Connect
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@ -58,7 +60,7 @@ const ConnectNodeDialog = () => {
|
|||||||
<Modal.Header>Connect Node</Modal.Header>
|
<Modal.Header>Connect Node</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<p>Run this command to get node id:</p>
|
<p>Run this command to get node id:</p>
|
||||||
<Code className="mt-2">docker exec -it garage /garage node id</Code>
|
<Code className="mt-2">docker exec garage /garage node id</Code>
|
||||||
|
|
||||||
<p className="mt-8">Enter node id:</p>
|
<p className="mt-8">Enter node id:</p>
|
||||||
<Input
|
<Input
|
||||||
|
@ -7,7 +7,7 @@ const ClusterPage = () => {
|
|||||||
const { data } = useClusterStatus();
|
const { data } = useClusterStatus();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="container">
|
||||||
<Page title="Cluster" />
|
<Page title="Cluster" />
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
|
@ -17,7 +17,7 @@ const HomePage = () => {
|
|||||||
const { data: health } = useNodesHealth();
|
const { data: health } = useNodesHealth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="container">
|
||||||
<Page title="Dashboard" />
|
<Page title="Dashboard" />
|
||||||
|
|
||||||
<section className="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-6">
|
<section className="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-6">
|
||||||
@ -27,7 +27,11 @@ const HomePage = () => {
|
|||||||
value={ucfirst(health?.status)}
|
value={ucfirst(health?.status)}
|
||||||
valueClassName={cn(
|
valueClassName={cn(
|
||||||
"text-lg",
|
"text-lg",
|
||||||
health?.status === "healthy" ? "text-success" : "text-error"
|
health?.status === "healthy"
|
||||||
|
? "text-success"
|
||||||
|
: health?.status === "degraded"
|
||||||
|
? "text-warning"
|
||||||
|
: "text-error"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<StatsCard title="Nodes" icon={HardDrive} value={health?.knownNodes} />
|
<StatsCard title="Nodes" icon={HardDrive} value={health?.knownNodes} />
|
||||||
|
32
src/types/garage.ts
Normal file
32
src/types/garage.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
export type Config = {
|
||||||
|
metadata_dir: string;
|
||||||
|
data_dir: string;
|
||||||
|
db_engine: string;
|
||||||
|
metadata_auto_snapshot_interval: string;
|
||||||
|
replication_factor: number;
|
||||||
|
compression_level: number;
|
||||||
|
rpc_bind_addr: string;
|
||||||
|
rpc_public_addr: string;
|
||||||
|
rpc_secret: string;
|
||||||
|
s3_api?: S3API;
|
||||||
|
s3_web?: S3Web;
|
||||||
|
admin?: Admin;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Admin = {
|
||||||
|
api_bind_addr: string;
|
||||||
|
admin_token: string;
|
||||||
|
metrics_token: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type S3API = {
|
||||||
|
s3_region: string;
|
||||||
|
api_bind_addr: string;
|
||||||
|
root_domain: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type S3Web = {
|
||||||
|
bind_addr: string;
|
||||||
|
root_domain: string;
|
||||||
|
index: string;
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user