mirror of
https://github.com/KohakuBlueleaf/KohakuHub.git
synced 2026-03-11 17:34:08 -05:00
fix ui and docker compose script
This commit is contained in:
@@ -51,6 +51,11 @@ python scripts/generate_docker_compose.py --config kohakuhub.conf
|
||||
- Auto-generated admin secret token
|
||||
- Option to use custom secrets
|
||||
|
||||
5. **Network:**
|
||||
- External Docker bridge network support
|
||||
- Allows cross-compose communication with external PostgreSQL/S3 services
|
||||
- Automatically added to hub-api and lakefs when using external services
|
||||
|
||||
**Example: Interactive Mode**
|
||||
|
||||
```
|
||||
@@ -197,6 +202,10 @@ secret_key = minioadmin
|
||||
[security]
|
||||
session_secret = your-secret-here
|
||||
admin_secret = your-admin-secret
|
||||
|
||||
[network]
|
||||
# Optional: for cross-compose communication
|
||||
external_network = shared-network
|
||||
```
|
||||
|
||||
For external PostgreSQL or S3:
|
||||
@@ -215,8 +224,40 @@ endpoint = https://your-s3-endpoint.com
|
||||
access_key = your-access-key
|
||||
secret_key = your-secret-key
|
||||
region = us-east-1
|
||||
|
||||
[network]
|
||||
# Required if external services are in different Docker Compose
|
||||
external_network = shared-network
|
||||
```
|
||||
|
||||
**Using External Docker Network:**
|
||||
|
||||
If PostgreSQL or S3 are in separate Docker Compose setups, you need a shared network:
|
||||
|
||||
```bash
|
||||
# Create the shared network first
|
||||
docker network create shared-network
|
||||
|
||||
# Your PostgreSQL docker-compose.yml
|
||||
services:
|
||||
postgres:
|
||||
# ... your config
|
||||
networks:
|
||||
- shared-network
|
||||
|
||||
networks:
|
||||
shared-network:
|
||||
external: true
|
||||
|
||||
# Generate KohakuHub with external network
|
||||
python scripts/generate_docker_compose.py --config kohakuhub.conf
|
||||
```
|
||||
|
||||
The generator will automatically:
|
||||
- Add the external network to `hub-api` and `lakefs` services
|
||||
- Configure them to use both `default` (hub-net) and the external network
|
||||
- Allow container name resolution across compose files
|
||||
|
||||
**Important Notes:**
|
||||
- Shell scripts automatically use LF line endings (configured in `.gitattributes`)
|
||||
- Database initialization runs automatically on LakeFS startup
|
||||
|
||||
@@ -146,7 +146,9 @@ def generate_lakefs_service(config: dict) -> str:
|
||||
force_path_style = "true"
|
||||
else:
|
||||
s3_endpoint = config["s3_endpoint"]
|
||||
force_path_style = "true" if "minio" in s3_endpoint.lower() else "false"
|
||||
# Use path-style for all non-AWS endpoints (MinIO, CloudFlare R2, custom S3)
|
||||
# Only AWS S3 (*.amazonaws.com) should use virtual-hosted style
|
||||
force_path_style = "false" if "amazonaws.com" in s3_endpoint.lower() else "true"
|
||||
|
||||
# Add entrypoint and volumes for database initialization
|
||||
entrypoint_config = ""
|
||||
@@ -160,6 +162,16 @@ def generate_lakefs_service(config: dict) -> str:
|
||||
- ./scripts/lakefs-entrypoint.sh:/scripts/lakefs-entrypoint.sh:ro
|
||||
- ./scripts/init-databases.sh:/scripts/init-databases.sh:ro"""
|
||||
|
||||
# Add external network if needed (for external postgres or s3)
|
||||
lakefs_networks_str = ""
|
||||
if config.get("external_network") and (
|
||||
not config["postgres_builtin"] or not config["s3_builtin"]
|
||||
):
|
||||
lakefs_networks_str = f""" networks:
|
||||
- default
|
||||
- {config['external_network']}
|
||||
"""
|
||||
|
||||
return f""" lakefs:
|
||||
image: treeverse/lakefs:latest
|
||||
container_name: lakefs
|
||||
@@ -180,7 +192,7 @@ def generate_lakefs_service(config: dict) -> str:
|
||||
user: "${{UID}}:${{GID}}"
|
||||
{depends_on_str} volumes:
|
||||
{volumes_config}
|
||||
"""
|
||||
{lakefs_networks_str}"""
|
||||
|
||||
|
||||
def generate_hub_api_service(config: dict) -> str:
|
||||
@@ -197,6 +209,16 @@ def generate_hub_api_service(config: dict) -> str:
|
||||
for dep in depends_on:
|
||||
depends_on_str += f" - {dep}\n"
|
||||
|
||||
# Add external network if needed (for external postgres or s3)
|
||||
networks_str = ""
|
||||
if config.get("external_network") and (
|
||||
not config["postgres_builtin"] or not config["s3_builtin"]
|
||||
):
|
||||
networks_str = f""" networks:
|
||||
- default
|
||||
- {config['external_network']}
|
||||
"""
|
||||
|
||||
# Database configuration
|
||||
if config["postgres_builtin"]:
|
||||
db_url = f"postgresql://{config['postgres_user']}:{config['postgres_password']}@postgres:5432/{config['postgres_db']}"
|
||||
@@ -261,7 +283,7 @@ def generate_hub_api_service(config: dict) -> str:
|
||||
- KOHAKU_HUB_DEFAULT_ORG_PUBLIC_QUOTA_BYTES=100_000_000
|
||||
volumes:
|
||||
- ./hub-meta/hub-api:/hub-api-creds
|
||||
"""
|
||||
{networks_str}"""
|
||||
|
||||
|
||||
def generate_hub_ui_service() -> str:
|
||||
@@ -299,8 +321,16 @@ def generate_docker_compose(config: dict) -> str:
|
||||
|
||||
content = "# docker-compose.yml\n# Generated by KohakuHub docker-compose generator\n\nservices:\n"
|
||||
content += "\n".join(services)
|
||||
|
||||
# Network configuration
|
||||
content += "\nnetworks:\n default:\n name: hub-net\n"
|
||||
|
||||
# Add external network if specified
|
||||
if config.get("external_network"):
|
||||
content += f""" {config['external_network']}:
|
||||
external: true
|
||||
"""
|
||||
|
||||
return content
|
||||
|
||||
|
||||
@@ -368,6 +398,13 @@ def load_config_file(config_path: Path) -> dict:
|
||||
config["session_secret"] = generate_secret()
|
||||
config["admin_secret"] = generate_secret()
|
||||
|
||||
# Network section
|
||||
if parser.has_section("network"):
|
||||
net = parser["network"]
|
||||
config["external_network"] = net.get("external_network", fallback="")
|
||||
else:
|
||||
config["external_network"] = ""
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@@ -418,6 +455,12 @@ secret_key = minioadmin
|
||||
# Session and admin secrets (auto-generated if not specified)
|
||||
# session_secret = your-session-secret-here
|
||||
# admin_secret = your-admin-secret-here
|
||||
|
||||
[network]
|
||||
# External bridge network (optional)
|
||||
# Use this if PostgreSQL or S3 are in different Docker Compose setups
|
||||
# Create the network first: docker network create shared-network
|
||||
# external_network = shared-network
|
||||
"""
|
||||
|
||||
output_path.write_text(template, encoding="utf-8")
|
||||
@@ -577,6 +620,26 @@ def interactive_config() -> dict:
|
||||
# LakeFS encryption key
|
||||
config["lakefs_encrypt_key"] = generate_secret()
|
||||
|
||||
# Network configuration
|
||||
print()
|
||||
print("--- Network Configuration ---")
|
||||
use_external_network = False
|
||||
if not config["postgres_builtin"] or not config["s3_builtin"]:
|
||||
use_external_network = ask_yes_no(
|
||||
"Use external Docker network for cross-compose communication?",
|
||||
default=False,
|
||||
)
|
||||
|
||||
if use_external_network:
|
||||
config["external_network"] = ask_string(
|
||||
"External network name", default="shared-network"
|
||||
)
|
||||
print()
|
||||
print(f"Note: Make sure the network exists:")
|
||||
print(f" docker network create {config['external_network']}")
|
||||
else:
|
||||
config["external_network"] = ""
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@@ -622,21 +685,34 @@ def generate_and_write_files(config: dict):
|
||||
print(f"S3 Storage: {'Built-in MinIO' if config['s3_builtin'] else 'Custom S3'}")
|
||||
if not config["s3_builtin"]:
|
||||
print(f" Endpoint: {config['s3_endpoint']}")
|
||||
if config.get("external_network"):
|
||||
print(f"External Network: {config['external_network']}")
|
||||
print(f"Session Secret: {config['session_secret'][:20]}...")
|
||||
print(f"Admin Secret: {config['admin_secret'][:20]}...")
|
||||
print("-" * 60)
|
||||
print()
|
||||
print("Next steps:")
|
||||
print("1. Review the generated docker-compose.yml")
|
||||
print("2. Build frontend: npm run build --prefix ./src/kohaku-hub-ui")
|
||||
print("3. Start services: docker-compose up -d")
|
||||
step_num = 1
|
||||
|
||||
if config.get("external_network"):
|
||||
print(f"{step_num}. Create external network if not exists:")
|
||||
print(f" docker network create {config['external_network']}")
|
||||
step_num += 1
|
||||
print()
|
||||
|
||||
print(f"{step_num}. Review the generated docker-compose.yml")
|
||||
step_num += 1
|
||||
print(f"{step_num}. Build frontend: npm run build --prefix ./src/kohaku-hub-ui")
|
||||
step_num += 1
|
||||
print(f"{step_num}. Start services: docker-compose up -d")
|
||||
print()
|
||||
if config["lakefs_use_postgres"]:
|
||||
print(" Note: Databases will be created automatically on first startup:")
|
||||
print(f" - {config['postgres_db']} (hub-api)")
|
||||
print(f" - {config['lakefs_db']} (LakeFS)")
|
||||
print()
|
||||
print("4. Access at: http://localhost:28080")
|
||||
step_num += 1
|
||||
print(f"{step_num}. Access at: http://localhost:28080")
|
||||
print()
|
||||
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
|
||||
<script setup>
|
||||
import hljs from "highlight.js/lib/core";
|
||||
import { copyToClipboard } from "@/utils/clipboard";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
|
||||
@@ -143,9 +144,13 @@ function escapeHtml(unsafe) {
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function copyCode() {
|
||||
navigator.clipboard.writeText(props.code);
|
||||
ElMessage.success("Code copied to clipboard");
|
||||
async function copyCode() {
|
||||
const success = await copyToClipboard(props.code);
|
||||
if (success) {
|
||||
ElMessage.success("Code copied to clipboard");
|
||||
} else {
|
||||
ElMessage.error("Failed to copy");
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for code or language changes
|
||||
|
||||
@@ -601,6 +601,7 @@ huggingface-cli download {{ repoInfo?.id }}</pre
|
||||
<script setup>
|
||||
import { repoAPI } from "@/utils/api";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { copyToClipboard } from "@/utils/clipboard";
|
||||
import MarkdownViewer from "@/components/common/MarkdownViewer.vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import dayjs from "dayjs";
|
||||
@@ -932,20 +933,32 @@ async function createReadme() {
|
||||
}
|
||||
}
|
||||
|
||||
function copyCloneUrl() {
|
||||
navigator.clipboard.writeText(cloneUrl.value);
|
||||
ElMessage.success("Clone URL copied to clipboard");
|
||||
async function copyCloneUrl() {
|
||||
const success = await copyToClipboard(cloneUrl.value);
|
||||
if (success) {
|
||||
ElMessage.success("Clone URL copied to clipboard");
|
||||
} else {
|
||||
ElMessage.error("Failed to copy");
|
||||
}
|
||||
}
|
||||
|
||||
function copyGitCloneUrl() {
|
||||
navigator.clipboard.writeText(gitCloneUrl.value);
|
||||
ElMessage.success("Git clone URL copied to clipboard");
|
||||
async function copyGitCloneUrl() {
|
||||
const success = await copyToClipboard(gitCloneUrl.value);
|
||||
if (success) {
|
||||
ElMessage.success("Git clone URL copied to clipboard");
|
||||
} else {
|
||||
ElMessage.error("Failed to copy");
|
||||
}
|
||||
}
|
||||
|
||||
function copyRepoId() {
|
||||
async function copyRepoId() {
|
||||
const repoId = `${props.namespace}/${props.name}`;
|
||||
navigator.clipboard.writeText(repoId);
|
||||
ElMessage.success("Repository ID copied to clipboard");
|
||||
const success = await copyToClipboard(repoId);
|
||||
if (success) {
|
||||
ElMessage.success("Repository ID copied to clipboard");
|
||||
} else {
|
||||
ElMessage.error("Failed to copy");
|
||||
}
|
||||
}
|
||||
|
||||
// Watchers
|
||||
|
||||
@@ -243,6 +243,7 @@
|
||||
<script setup>
|
||||
import MarkdownViewer from "@/components/common/MarkdownViewer.vue";
|
||||
import CodeViewer from "@/components/common/CodeViewer.vue";
|
||||
import { copyToClipboard } from "@/utils/clipboard";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
@@ -515,15 +516,23 @@ function downloadFile() {
|
||||
window.open(fileUrl.value, "_blank");
|
||||
}
|
||||
|
||||
function copyFileUrl() {
|
||||
async function copyFileUrl() {
|
||||
const fullUrl = window.location.origin + fileUrl.value;
|
||||
navigator.clipboard.writeText(fullUrl);
|
||||
ElMessage.success("File URL copied to clipboard");
|
||||
const success = await copyToClipboard(fullUrl);
|
||||
if (success) {
|
||||
ElMessage.success("File URL copied to clipboard");
|
||||
} else {
|
||||
ElMessage.error("Failed to copy");
|
||||
}
|
||||
}
|
||||
|
||||
function copyContent() {
|
||||
navigator.clipboard.writeText(fileContent.value);
|
||||
ElMessage.success("Content copied to clipboard");
|
||||
async function copyContent() {
|
||||
const success = await copyToClipboard(fileContent.value);
|
||||
if (success) {
|
||||
ElMessage.success("Content copied to clipboard");
|
||||
} else {
|
||||
ElMessage.error("Failed to copy");
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToFolder(folderPath) {
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useRouter } from "vue-router";
|
||||
import { authAPI, settingsAPI } from "@/utils/api";
|
||||
import { copyToClipboard } from "@/utils/clipboard";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
@@ -237,9 +238,13 @@ async function handleRevokeToken(id) {
|
||||
}
|
||||
}
|
||||
|
||||
function copyToken() {
|
||||
navigator.clipboard.writeText(newToken.value);
|
||||
ElMessage.success("Token copied to clipboard");
|
||||
async function copyToken() {
|
||||
const success = await copyToClipboard(newToken.value);
|
||||
if (success) {
|
||||
ElMessage.success("Token copied to clipboard");
|
||||
} else {
|
||||
ElMessage.error("Failed to copy token");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
48
src/kohaku-hub-ui/src/utils/clipboard.js
Normal file
48
src/kohaku-hub-ui/src/utils/clipboard.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copy text to clipboard with fallback for non-secure contexts
|
||||
* @param {string} text - Text to copy
|
||||
* @returns {Promise<boolean>} - True if successful
|
||||
*/
|
||||
export async function copyToClipboard(text) {
|
||||
// Try modern Clipboard API first (requires HTTPS or localhost)
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"Clipboard API failed, falling back to textarea method:",
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for non-secure contexts (HTTP)
|
||||
return copyToClipboardFallback(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback method using textarea (works in non-secure contexts)
|
||||
* @param {string} text - Text to copy
|
||||
* @returns {boolean} - True if successful
|
||||
*/
|
||||
function copyToClipboardFallback(text) {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.left = "-9999px";
|
||||
textarea.style.top = "-9999px";
|
||||
document.body.appendChild(textarea);
|
||||
|
||||
try {
|
||||
textarea.select();
|
||||
textarea.setSelectionRange(0, text.length);
|
||||
const successful = document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
return successful;
|
||||
} catch (err) {
|
||||
console.error("Fallback copy failed:", err);
|
||||
document.body.removeChild(textarea);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user