Compare commits
91 Commits
26880d2e09
...
11e8c8e132
| Author | SHA1 | Date | |
|---|---|---|---|
| 11e8c8e132 | |||
| 40c2e2414d | |||
| 066ef5eab5 | |||
| 2946ee9b42 | |||
| 5f70a239a7 | |||
| d67d24e7e6 | |||
| 01efc9dae1 | |||
| 43814511ee | |||
| db47e4ce55 | |||
| d5a5c3b902 | |||
| 066c37087d | |||
| b40649b9ed | |||
| 197647d97a | |||
| e9e529e322 | |||
| bc3ff29d2f | |||
| eb8ed5da59 | |||
| b6ae41d576 | |||
| cf42de3060 | |||
| 6062712a92 | |||
| 7790a2dc51 | |||
| a58bef5c73 | |||
| 232dd947d8 | |||
| 33966548d7 | |||
| cad6e97464 | |||
| 47a3213fb9 | |||
| 303d7272f8 | |||
| 0711ded532 | |||
| 495857bbee | |||
| df7582a8db | |||
| 3802b0af8c | |||
| 1702f3a5e9 | |||
| 83c24d4b85 | |||
| dd596698e5 | |||
| 0fee26a846 | |||
| 35096b5e23 | |||
| 0299728d72 | |||
| de6ff0d41b | |||
| 260a90aac5 | |||
| 56a1508b38 | |||
| 7c0a601499 | |||
| a5b1bba226 | |||
| 8f13ed3031 | |||
| 305b07bf8b | |||
| 4e1992f489 | |||
| ac7a6cfadc | |||
| 29daebd628 | |||
| 71873b7bb3 | |||
| 68b25c90a8 | |||
| e9b8794f1a | |||
| 6d94d26e40 | |||
| 598dd50d1d | |||
| eacb88a0e3 | |||
| 10676b7029 | |||
| 2023f6c211 | |||
| 2111c12d0a | |||
| b46133e4c1 | |||
| 6cc2fb4c9b | |||
| c406f71988 | |||
| 55bdd981b1 | |||
| 15843a4607 | |||
| 8b81bb9f1f | |||
| 7f244a8252 | |||
| c0505b4d4f | |||
| 1b863bea1b | |||
| 7c6c959774 | |||
| 554e1128b8 | |||
| 55ca476972 | |||
| 4b4d23e671 | |||
| e80369c8b5 | |||
| ffe942c9dd | |||
| 19744796e4 | |||
| a6bebe295c | |||
| e2ce346b7b | |||
| 789e5a90e3 | |||
| 163d03e6f5 | |||
| 169237b31b | |||
| 45bb8c5429 | |||
| 17c28433bd | |||
| 077daa0245 | |||
| d38cda09dd | |||
| 1a893c0bc0 | |||
| 40ed375aa4 | |||
| 5fdc92fcdf | |||
| e83328b4ff | |||
| 687f4ce565 | |||
| dc9e9e0588 | |||
| 2de3e53ab2 | |||
| 40ea0580d9 | |||
| 8da906738b | |||
| d5a1c0e091 | |||
| ef7a0fcf92 |
Binary file not shown.
@@ -24,7 +24,7 @@ class EncryptionManager:
|
|||||||
# @PRE: ENCRYPTION_KEY env var must be set or use default dev key.
|
# @PRE: ENCRYPTION_KEY env var must be set or use default dev key.
|
||||||
# @POST: Fernet instance ready for encryption/decryption.
|
# @POST: Fernet instance ready for encryption/decryption.
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.key = os.getenv("ENCRYPTION_KEY", "ZcytYzi0iHIl4Ttr-GdAEk117aGRogkGvN3wiTxrPpE=").encode()
|
self.key = os.getenv("ENCRYPTION_KEY", "REMOVED_HISTORICAL_SECRET_DO_NOT_USE").encode()
|
||||||
self.fernet = Fernet(self.key)
|
self.fernet = Fernet(self.key)
|
||||||
# [/DEF:EncryptionManager.__init__:Function]
|
# [/DEF:EncryptionManager.__init__:Function]
|
||||||
|
|
||||||
|
|||||||
BIN
backend/tasks.db
BIN
backend/tasks.db
Binary file not shown.
@@ -1,76 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Debug script to test Superset API authentication"""
|
|
||||||
|
|
||||||
from pprint import pprint
|
|
||||||
from src.core.superset_client import SupersetClient
|
|
||||||
from src.core.config_manager import ConfigManager
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("Debugging Superset API authentication...")
|
|
||||||
|
|
||||||
config = ConfigManager()
|
|
||||||
|
|
||||||
# Select first available environment
|
|
||||||
environments = config.get_environments()
|
|
||||||
|
|
||||||
if not environments:
|
|
||||||
print("No environments configured")
|
|
||||||
return
|
|
||||||
|
|
||||||
env = environments[0]
|
|
||||||
print(f"\nTesting environment: {env.name}")
|
|
||||||
print(f"URL: {env.url}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Test API client authentication
|
|
||||||
print("\n--- Testing API Authentication ---")
|
|
||||||
client = SupersetClient(env)
|
|
||||||
tokens = client.authenticate()
|
|
||||||
|
|
||||||
print("\nAPI Auth Success!")
|
|
||||||
print(f"Access Token: {tokens.get('access_token', 'N/A')}")
|
|
||||||
print(f"CSRF Token: {tokens.get('csrf_token', 'N/A')}")
|
|
||||||
|
|
||||||
# Debug cookies from session
|
|
||||||
print("\n--- Session Cookies ---")
|
|
||||||
for cookie in client.network.session.cookies:
|
|
||||||
print(f"{cookie.name}={cookie.value}")
|
|
||||||
|
|
||||||
# Test accessing UI via requests
|
|
||||||
print("\n--- Testing UI Access ---")
|
|
||||||
ui_url = env.url.rstrip('/').replace('/api/v1', '')
|
|
||||||
print(f"UI URL: {ui_url}")
|
|
||||||
|
|
||||||
# Try to access UI home page
|
|
||||||
ui_response = client.network.session.get(ui_url, timeout=30, allow_redirects=True)
|
|
||||||
print(f"Status Code: {ui_response.status_code}")
|
|
||||||
print(f"URL: {ui_response.url}")
|
|
||||||
|
|
||||||
# Check response headers
|
|
||||||
print("\n--- Response Headers ---")
|
|
||||||
pprint(dict(ui_response.headers))
|
|
||||||
|
|
||||||
print("\n--- Response Content Preview (200 chars) ---")
|
|
||||||
print(repr(ui_response.text[:200]))
|
|
||||||
|
|
||||||
if ui_response.status_code == 200:
|
|
||||||
print("\nUI Access: Success")
|
|
||||||
|
|
||||||
# Try to access a dashboard
|
|
||||||
# For testing, just use the home page
|
|
||||||
print("\n--- Checking if login is required ---")
|
|
||||||
if "login" in ui_response.url.lower() or "login" in ui_response.text.lower():
|
|
||||||
print("❌ Not logged in to UI")
|
|
||||||
else:
|
|
||||||
print("✅ Logged in to UI")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\n❌ Error: {type(e).__name__}: {e}")
|
|
||||||
import traceback
|
|
||||||
print("\nStack Trace:")
|
|
||||||
print(traceback.format_exc())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Test script to debug API key decryption issue."""
|
|
||||||
|
|
||||||
from src.core.database import SessionLocal
|
|
||||||
from src.models.llm import LLMProvider
|
|
||||||
from cryptography.fernet import Fernet
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Get the encryption key
|
|
||||||
key = os.getenv("ENCRYPTION_KEY", "ZcytYzi0iHIl4Ttr-GdAEk117aGRogkGvN3wiTxrPpE=").encode()
|
|
||||||
print(f"Encryption key (first 20 chars): {key[:20]}")
|
|
||||||
print(f"Encryption key length: {len(key)}")
|
|
||||||
|
|
||||||
# Create Fernet instance
|
|
||||||
fernet = Fernet(key)
|
|
||||||
|
|
||||||
# Get provider from database
|
|
||||||
db = SessionLocal()
|
|
||||||
provider = db.query(LLMProvider).filter(LLMProvider.id == '6c899741-4108-4196-aea4-f38ad2f0150e').first()
|
|
||||||
|
|
||||||
if provider:
|
|
||||||
print("\nProvider found:")
|
|
||||||
print(f" ID: {provider.id}")
|
|
||||||
print(f" Name: {provider.name}")
|
|
||||||
print(f" Encrypted API Key (first 50 chars): {provider.api_key[:50]}")
|
|
||||||
print(f" Encrypted API Key Length: {len(provider.api_key)}")
|
|
||||||
|
|
||||||
# Test decryption
|
|
||||||
print("\nAttempting decryption...")
|
|
||||||
try:
|
|
||||||
decrypted = fernet.decrypt(provider.api_key.encode()).decode()
|
|
||||||
print("Decryption successful!")
|
|
||||||
print(f" Decrypted key length: {len(decrypted)}")
|
|
||||||
print(f" Decrypted key (first 8 chars): {decrypted[:8]}")
|
|
||||||
print(f" Decrypted key is empty: {len(decrypted) == 0}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Decryption failed with error: {e}")
|
|
||||||
print(f"Error type: {type(e).__name__}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
else:
|
|
||||||
print("Provider not found")
|
|
||||||
|
|
||||||
db.close()
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"key[": 20, ")\n\n# Create Fernet instance\nfernet = Fernet(key)\n\n# Test encrypting an empty string\nempty_encrypted = fernet.encrypt(b\"": ".", "print(f": "nEncrypted empty string: {empty_encrypted"}, {"test-api-key-12345\"\ntest_encrypted = fernet.encrypt(test_key.encode()).decode()\nprint(f": "nEncrypted test key: {test_encrypted"}, {"gAAAAABphhwSZie0OwXjJ78Fk-c4Uo6doNJXipX49AX7Bypzp4ohiRX3hXPXKb45R1vhNUOqbm6Ke3-eRwu_KdWMZ9chFBKmqw==\"\nprint(f": "nStored encrypted key: {stored_key"}, {"len(stored_key)}": "Check if stored key matches empty string encryption\nif stored_key == empty_encrypted:\n print(", "string!": "else:\n print(", "print(f": "mpty string encryption: {empty_encrypted"}, {"stored_key}": "Try to decrypt the stored key\ntry:\n decrypted = fernet.decrypt(stored_key.encode()).decode()\n print(f", "print(f": "ecrypted key length: {len(decrypted)"}, {")\nexcept Exception as e:\n print(f": "nDecryption failed with error: {e"}]
|
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<link rel="icon" type="image/svg+xml" href="%sveltekit.assets%/favicon.svg" />
|
||||||
|
<link rel="alternate icon" type="image/png" href="%sveltekit.assets%/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
import { t, _ } from "$lib/i18n";
|
import { t, _ } from "$lib/i18n";
|
||||||
|
import Icon from "$lib/ui/Icon.svelte";
|
||||||
|
|
||||||
let { maxVisible = 3 } = $props();
|
let { maxVisible = 3 } = $props();
|
||||||
|
|
||||||
@@ -82,30 +83,103 @@
|
|||||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
.join(" ");
|
.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCrumbMeta(item) {
|
||||||
|
if (item.path === "/") {
|
||||||
|
return {
|
||||||
|
icon: "home",
|
||||||
|
tone: "from-sky-100 to-cyan-100 text-sky-700 ring-sky-200",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const segment = item.path.split("/").filter(Boolean).at(-1) || "";
|
||||||
|
|
||||||
|
const map = {
|
||||||
|
dashboards: {
|
||||||
|
icon: "dashboard",
|
||||||
|
tone: "from-sky-100 to-sky-200 text-sky-700 ring-sky-200",
|
||||||
|
},
|
||||||
|
datasets: {
|
||||||
|
icon: "database",
|
||||||
|
tone: "from-emerald-100 to-emerald-200 text-emerald-700 ring-emerald-200",
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
icon: "storage",
|
||||||
|
tone: "from-amber-100 to-amber-200 text-amber-800 ring-amber-200",
|
||||||
|
},
|
||||||
|
reports: {
|
||||||
|
icon: "reports",
|
||||||
|
tone: "from-violet-100 to-fuchsia-100 text-violet-700 ring-violet-200",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
icon: "admin",
|
||||||
|
tone: "from-rose-100 to-rose-200 text-rose-700 ring-rose-200",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
icon: "settings",
|
||||||
|
tone: "from-slate-100 to-slate-200 text-slate-700 ring-slate-200",
|
||||||
|
},
|
||||||
|
git: {
|
||||||
|
icon: "storage",
|
||||||
|
tone: "from-orange-100 to-orange-200 text-orange-700 ring-orange-200",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
map[segment] || {
|
||||||
|
icon: "layers",
|
||||||
|
tone: "from-slate-100 to-slate-200 text-slate-600 ring-slate-200",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
class="flex items-center space-x-2 text-sm text-gray-600"
|
class="mx-4 md:mx-6"
|
||||||
aria-label="Breadcrumb navigation"
|
aria-label="Breadcrumb navigation"
|
||||||
>
|
>
|
||||||
|
<div class="inline-flex max-w-full items-center gap-1.5 rounded-xl border border-slate-200/80 bg-white/85 px-2 py-1.5 shadow-sm backdrop-blur">
|
||||||
{#each breadcrumbItems as item, index}
|
{#each breadcrumbItems as item, index}
|
||||||
<div class="flex items-center">
|
<div class="flex min-w-0 items-center gap-1.5">
|
||||||
{#if item.isEllipsis}
|
{#if item.isEllipsis}
|
||||||
<span class="text-gray-400">...</span>
|
<span class="px-2 py-1 text-xs font-semibold tracking-wide text-slate-400"
|
||||||
{:else if item.isLast}
|
>...</span
|
||||||
<span class="text-gray-900 font-medium">{item.label}</span>
|
>
|
||||||
|
{:else}
|
||||||
|
{@const meta = getCrumbMeta(item)}
|
||||||
|
{#if item.isLast}
|
||||||
|
<span
|
||||||
|
class="inline-flex min-w-0 items-center gap-2 rounded-lg bg-slate-900 px-2.5 py-1.5 text-sm font-medium text-white"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-md bg-white/10"
|
||||||
|
>
|
||||||
|
<Icon name={meta.icon} size={12} strokeWidth={2.1} />
|
||||||
|
</span>
|
||||||
|
<span class="truncate">{item.label}</span>
|
||||||
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
<a
|
<a
|
||||||
href={item.path}
|
href={item.path}
|
||||||
class="hover:text-primary hover:underline cursor-pointer transition-colors"
|
class="inline-flex min-w-0 items-center gap-2 rounded-lg px-2.5 py-1.5 text-sm text-slate-700 ring-1 ring-transparent transition-all hover:bg-slate-50 hover:ring-slate-200"
|
||||||
>{item.label}</a
|
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
class="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-md bg-gradient-to-br ring-1 {meta.tone}"
|
||||||
|
>
|
||||||
|
<Icon name={meta.icon} size={12} strokeWidth={2.1} />
|
||||||
|
</span>
|
||||||
|
<span class="truncate">{item.label}</span>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if index < breadcrumbItems.length - 1}
|
{#if index < breadcrumbItems.length - 1}
|
||||||
<span class="text-gray-400">/</span>
|
<span class="text-slate-300">
|
||||||
|
<Icon name="chevronRight" size={14} strokeWidth={2.1} />
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- [/DEF:Breadcrumbs:Component] -->
|
<!-- [/DEF:Breadcrumbs:Component] -->
|
||||||
|
|||||||
@@ -24,13 +24,15 @@
|
|||||||
} from "$lib/stores/sidebar.js";
|
} from "$lib/stores/sidebar.js";
|
||||||
import { t } from "$lib/i18n";
|
import { t } from "$lib/i18n";
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
|
import Icon from "$lib/ui/Icon.svelte";
|
||||||
|
|
||||||
// Sidebar categories with sub-items matching Superset-style navigation
|
function buildCategories() {
|
||||||
let categories = [
|
return [
|
||||||
{
|
{
|
||||||
id: "dashboards",
|
id: "dashboards",
|
||||||
label: $t.nav?.dashboards || "DASHBOARDS",
|
label: $t.nav?.dashboards || "DASHBOARDS",
|
||||||
icon: "M3 3h18v18H3V3zm16 16V5H5v14h14z",
|
icon: "dashboard",
|
||||||
|
tone: "from-sky-100 to-sky-200 text-sky-700 ring-sky-200",
|
||||||
path: "/dashboards",
|
path: "/dashboards",
|
||||||
subItems: [
|
subItems: [
|
||||||
{ label: $t.nav?.overview || "Overview", path: "/dashboards" },
|
{ label: $t.nav?.overview || "Overview", path: "/dashboards" },
|
||||||
@@ -39,7 +41,8 @@
|
|||||||
{
|
{
|
||||||
id: "datasets",
|
id: "datasets",
|
||||||
label: $t.nav?.datasets || "DATASETS",
|
label: $t.nav?.datasets || "DATASETS",
|
||||||
icon: "M3 3h18v18H3V3zm2 2v14h14V5H5zm2 2h10v2H7V7zm0 4h10v2H7v-2zm0 4h6v2H7v-2z",
|
icon: "database",
|
||||||
|
tone: "from-emerald-100 to-emerald-200 text-emerald-700 ring-emerald-200",
|
||||||
path: "/datasets",
|
path: "/datasets",
|
||||||
subItems: [
|
subItems: [
|
||||||
{ label: $t.nav?.all_datasets || "All Datasets", path: "/datasets" },
|
{ label: $t.nav?.all_datasets || "All Datasets", path: "/datasets" },
|
||||||
@@ -48,7 +51,8 @@
|
|||||||
{
|
{
|
||||||
id: "storage",
|
id: "storage",
|
||||||
label: $t.nav?.storage || "STORAGE",
|
label: $t.nav?.storage || "STORAGE",
|
||||||
icon: "M4 4h16v16H4V4zm2 2v12h12V6H6zm2 2h8v2H8V8zm0 4h8v2H8v-2zm0 4h5v2H8v-2z",
|
icon: "storage",
|
||||||
|
tone: "from-amber-100 to-amber-200 text-amber-800 ring-amber-200",
|
||||||
path: "/storage",
|
path: "/storage",
|
||||||
subItems: [
|
subItems: [
|
||||||
{ label: $t.nav?.backups || "Backups", path: "/storage/backups" },
|
{ label: $t.nav?.backups || "Backups", path: "/storage/backups" },
|
||||||
@@ -61,14 +65,16 @@
|
|||||||
{
|
{
|
||||||
id: "reports",
|
id: "reports",
|
||||||
label: $t.nav?.reports || "REPORTS",
|
label: $t.nav?.reports || "REPORTS",
|
||||||
icon: "M4 5h16M4 12h16M4 19h10",
|
icon: "reports",
|
||||||
|
tone: "from-violet-100 to-fuchsia-100 text-violet-700 ring-violet-200",
|
||||||
path: "/reports",
|
path: "/reports",
|
||||||
subItems: [{ label: $t.nav?.reports || "Reports", path: "/reports" }],
|
subItems: [{ label: $t.nav?.reports || "Reports", path: "/reports" }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "admin",
|
id: "admin",
|
||||||
label: $t.nav?.admin || "ADMIN",
|
label: $t.nav?.admin || "ADMIN",
|
||||||
icon: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z",
|
icon: "admin",
|
||||||
|
tone: "from-rose-100 to-rose-200 text-rose-700 ring-rose-200",
|
||||||
path: "/admin",
|
path: "/admin",
|
||||||
subItems: [
|
subItems: [
|
||||||
{ label: $t.nav?.admin_users || "Users", path: "/admin/users" },
|
{ label: $t.nav?.admin_users || "Users", path: "/admin/users" },
|
||||||
@@ -77,6 +83,9 @@
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
let categories = buildCategories();
|
||||||
|
|
||||||
let isExpanded = true;
|
let isExpanded = true;
|
||||||
let activeCategory = "dashboards";
|
let activeCategory = "dashboards";
|
||||||
@@ -93,57 +102,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reactive categories to update translations
|
// Reactive categories to update translations
|
||||||
$: categories = [
|
$: categories = buildCategories();
|
||||||
{
|
|
||||||
id: "dashboards",
|
|
||||||
label: $t.nav?.dashboards || "DASHBOARDS",
|
|
||||||
icon: "M3 3h18v18H3V3zm16 16V5H5v14h14z",
|
|
||||||
path: "/dashboards",
|
|
||||||
subItems: [
|
|
||||||
{ label: $t.nav?.overview || "Overview", path: "/dashboards" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "datasets",
|
|
||||||
label: $t.nav?.datasets || "DATASETS",
|
|
||||||
icon: "M3 3h18v18H3V3zm2 2v14h14V5H5zm2 2h10v2H7V7zm0 4h10v2H7v-2zm0 4h6v2H7v-2z",
|
|
||||||
path: "/datasets",
|
|
||||||
subItems: [
|
|
||||||
{ label: $t.nav?.all_datasets || "All Datasets", path: "/datasets" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "storage",
|
|
||||||
label: $t.nav?.storage || "STORAGE",
|
|
||||||
icon: "M4 4h16v16H4V4zm2 2v12h12V6H6zm2 2h8v2H8V8zm0 4h8v2H8v-2zm0 4h5v2H8v-2z",
|
|
||||||
path: "/storage",
|
|
||||||
subItems: [
|
|
||||||
{ label: $t.nav?.backups || "Backups", path: "/storage/backups" },
|
|
||||||
{
|
|
||||||
label: $t.nav?.repositories || "Repositories",
|
|
||||||
path: "/storage/repos",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "reports",
|
|
||||||
label: $t.nav?.reports || "REPORTS",
|
|
||||||
icon: "M4 5h16M4 12h16M4 19h10",
|
|
||||||
path: "/reports",
|
|
||||||
subItems: [{ label: $t.nav?.reports || "Reports", path: "/reports" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "admin",
|
|
||||||
label: $t.nav?.admin || "ADMIN",
|
|
||||||
icon: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z",
|
|
||||||
path: "/admin",
|
|
||||||
subItems: [
|
|
||||||
{ label: $t.nav?.admin_users || "Users", path: "/admin/users" },
|
|
||||||
{ label: $t.nav?.admin_roles || "Roles", path: "/admin/roles" },
|
|
||||||
{ label: $t.nav?.settings || "Settings", path: "/settings" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Update active item when page changes
|
// Update active item when page changes
|
||||||
$: if ($page && $page.url.pathname !== activeItem) {
|
$: if ($page && $page.url.pathname !== activeItem) {
|
||||||
@@ -238,7 +197,12 @@
|
|||||||
: 'justify-center'}"
|
: 'justify-center'}"
|
||||||
>
|
>
|
||||||
{#if isExpanded}
|
{#if isExpanded}
|
||||||
<span class="font-semibold text-gray-800">Menu</span>
|
<span class="font-semibold text-gray-800 flex items-center gap-2">
|
||||||
|
<span class="inline-flex h-6 w-6 items-center justify-center rounded-md bg-gradient-to-br from-slate-100 to-slate-200 text-slate-700 ring-1 ring-slate-200">
|
||||||
|
<Icon name="layers" size={14} />
|
||||||
|
</span>
|
||||||
|
Menu
|
||||||
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-xs text-gray-500">M</span>
|
<span class="text-xs text-gray-500">M</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -264,16 +228,9 @@
|
|||||||
aria-expanded={expandedCategories.has(category.id)}
|
aria-expanded={expandedCategories.has(category.id)}
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<svg
|
<span class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br ring-1 transition-all {category.tone}">
|
||||||
class="w-5 h-5 shrink-0"
|
<Icon name={category.icon} size={16} strokeWidth={2} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
</span>
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path d={category.icon} />
|
|
||||||
</svg>
|
|
||||||
{#if isExpanded}
|
{#if isExpanded}
|
||||||
<span class="ml-3 text-sm font-medium truncate"
|
<span class="ml-3 text-sm font-medium truncate"
|
||||||
>{category.label}</span
|
>{category.label}</span
|
||||||
@@ -281,22 +238,15 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if isExpanded}
|
{#if isExpanded}
|
||||||
<svg
|
<Icon
|
||||||
|
name="chevronDown"
|
||||||
|
size={16}
|
||||||
class="text-gray-400 transition-transform duration-200 {expandedCategories.has(
|
class="text-gray-400 transition-transform duration-200 {expandedCategories.has(
|
||||||
category.id,
|
category.id,
|
||||||
)
|
)
|
||||||
? 'rotate-180'
|
? 'rotate-180'
|
||||||
: ''}"
|
: ''}"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
/>
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path d="M6 9l6 6 6-6" />
|
|
||||||
</svg>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -332,18 +282,9 @@
|
|||||||
class="flex items-center justify-center w-full px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
class="flex items-center justify-center w-full px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
on:click={handleToggleClick}
|
on:click={handleToggleClick}
|
||||||
>
|
>
|
||||||
<svg
|
<span class="mr-2 inline-flex h-6 w-6 items-center justify-center rounded-md bg-slate-100 text-slate-600">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<Icon name="chevronLeft" size={14} />
|
||||||
width="16"
|
</span>
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
class="mr-2"
|
|
||||||
>
|
|
||||||
<path d="M15 18l-6-6 6-6" />
|
|
||||||
</svg>
|
|
||||||
Collapse
|
Collapse
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -354,17 +295,7 @@
|
|||||||
on:click={handleToggleClick}
|
on:click={handleToggleClick}
|
||||||
aria-label="Expand sidebar"
|
aria-label="Expand sidebar"
|
||||||
>
|
>
|
||||||
<svg
|
<Icon name="chevronRight" size={16} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path d="M9 18l6-6-6-6" />
|
|
||||||
</svg>
|
|
||||||
<span class="ml-2">Expand</span>
|
<span class="ml-2">Expand</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
import PasswordPrompt from "../../../components/PasswordPrompt.svelte";
|
import PasswordPrompt from "../../../components/PasswordPrompt.svelte";
|
||||||
import { t } from "$lib/i18n";
|
import { t } from "$lib/i18n";
|
||||||
import { api } from "$lib/api.js";
|
import { api } from "$lib/api.js";
|
||||||
|
import Icon from "$lib/ui/Icon.svelte";
|
||||||
|
|
||||||
let isOpen = false;
|
let isOpen = false;
|
||||||
let activeTaskId = null;
|
let activeTaskId = null;
|
||||||
@@ -209,9 +210,7 @@
|
|||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
{#if !activeTaskId && recentTasks.length > 0}
|
{#if !activeTaskId && recentTasks.length > 0}
|
||||||
<span class="flex items-center justify-center p-1.5 mr-1 text-cyan-400">
|
<span class="flex items-center justify-center p-1.5 mr-1 text-cyan-400">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<Icon name="list" size={16} strokeWidth={2} />
|
||||||
<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
</span>
|
||||||
{:else if activeTaskId}
|
{:else if activeTaskId}
|
||||||
<button
|
<button
|
||||||
@@ -219,17 +218,7 @@
|
|||||||
on:click={goBackToList}
|
on:click={goBackToList}
|
||||||
aria-label="Back to task list"
|
aria-label="Back to task list"
|
||||||
>
|
>
|
||||||
<svg
|
<Icon name="back" size={16} strokeWidth={2} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<h2 class="text-sm font-semibold text-slate-100 tracking-tight">
|
<h2 class="text-sm font-semibold text-slate-100 tracking-tight">
|
||||||
@@ -256,17 +245,7 @@
|
|||||||
on:click={handleClose}
|
on:click={handleClose}
|
||||||
aria-label="Close drawer"
|
aria-label="Close drawer"
|
||||||
>
|
>
|
||||||
<svg
|
<Icon name="close" size={18} strokeWidth={2} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path d="M18 6L6 18M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -301,18 +280,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col items-center justify-center h-full text-slate-500">
|
<div class="flex flex-col items-center justify-center h-full text-slate-500">
|
||||||
<svg
|
<Icon
|
||||||
class="w-12 h-12 mb-3 text-slate-700"
|
name="clipboard"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
size={48}
|
||||||
viewBox="0 0 24 24"
|
strokeWidth={1.6}
|
||||||
fill="none"
|
className="mb-3 text-slate-700"
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
|
||||||
<p>{$t.tasks?.select_task || 'No recent tasks'}</p>
|
<p>{$t.tasks?.select_task || 'No recent tasks'}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -330,4 +303,3 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- [/DEF:TaskDrawer:Component] -->
|
<!-- [/DEF:TaskDrawer:Component] -->
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
import { sidebarStore, toggleMobileSidebar } from "$lib/stores/sidebar.js";
|
import { sidebarStore, toggleMobileSidebar } from "$lib/stores/sidebar.js";
|
||||||
import { t } from "$lib/i18n";
|
import { t } from "$lib/i18n";
|
||||||
import { auth } from "$lib/auth/store.js";
|
import { auth } from "$lib/auth/store.js";
|
||||||
|
import Icon from "$lib/ui/Icon.svelte";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
@@ -99,19 +100,7 @@
|
|||||||
on:click={handleHamburgerClick}
|
on:click={handleHamburgerClick}
|
||||||
aria-label="Toggle menu"
|
aria-label="Toggle menu"
|
||||||
>
|
>
|
||||||
<svg
|
<Icon name="menu" size={22} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
|
||||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
|
||||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Logo/Brand -->
|
<!-- Logo/Brand -->
|
||||||
@@ -119,14 +108,9 @@
|
|||||||
href="/"
|
href="/"
|
||||||
class="flex items-center text-xl font-bold text-gray-800 hover:text-primary transition-colors"
|
class="flex items-center text-xl font-bold text-gray-800 hover:text-primary transition-colors"
|
||||||
>
|
>
|
||||||
<svg
|
<span class="mr-2 inline-flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-sky-500 via-cyan-500 to-indigo-600 text-white shadow-sm">
|
||||||
class="w-8 h-8 mr-2 text-primary"
|
<Icon name="layers" size={18} strokeWidth={2.1} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
</span>
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
|
|
||||||
</svg>
|
|
||||||
<span>Superset Tools</span>
|
<span>Superset Tools</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,7 +131,7 @@
|
|||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<!-- Activity Indicator -->
|
<!-- Activity Indicator -->
|
||||||
<div
|
<div
|
||||||
class="relative cursor-pointer p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
class="relative cursor-pointer p-2 rounded-lg hover:bg-gray-100 transition-colors text-slate-600"
|
||||||
on:click={handleActivityClick}
|
on:click={handleActivityClick}
|
||||||
on:keydown={(e) =>
|
on:keydown={(e) =>
|
||||||
(e.key === "Enter" || e.key === " ") && handleActivityClick()}
|
(e.key === "Enter" || e.key === " ") && handleActivityClick()}
|
||||||
@@ -155,18 +139,7 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
aria-label="Activity"
|
aria-label="Activity"
|
||||||
>
|
>
|
||||||
<svg
|
<Icon name="activity" size={22} />
|
||||||
class="w-6 h-6 text-gray-600"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{#if activeCount > 0}
|
{#if activeCount > 0}
|
||||||
<span
|
<span
|
||||||
class="absolute -top-1 -right-1 bg-destructive text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center"
|
class="absolute -top-1 -right-1 bg-destructive text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center"
|
||||||
|
|||||||
66
frontend/src/lib/ui/Icon.svelte
Normal file
66
frontend/src/lib/ui/Icon.svelte
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<script>
|
||||||
|
export let name = "circle";
|
||||||
|
export let size = 20;
|
||||||
|
export let className = "";
|
||||||
|
export let strokeWidth = 1.9;
|
||||||
|
|
||||||
|
const iconPaths = {
|
||||||
|
home: ["M3 11l9-7 9 7", "M5 10v9h14v-9", "M10 19v-5h4v5"],
|
||||||
|
dashboard: ["M4 4h16v16H4z", "M4 10h16", "M10 4v16"],
|
||||||
|
database: [
|
||||||
|
"M4 7c0-1.7 3.6-3 8-3s8 1.3 8 3-3.6 3-8 3-8-1.3-8-3z",
|
||||||
|
"M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3",
|
||||||
|
"M4 17c0 1.7 3.6 3 8 3s8-1.3 8-3",
|
||||||
|
"M4 7v10",
|
||||||
|
"M20 7v10",
|
||||||
|
],
|
||||||
|
storage: [
|
||||||
|
"M3 8l9-4 9 4-9 4-9-4z",
|
||||||
|
"M3 13l9 4 9-4",
|
||||||
|
"M3 17l9 4 9-4",
|
||||||
|
],
|
||||||
|
reports: ["M5 5h14v14H5z", "M8 9h8", "M8 13h8", "M8 17h5"],
|
||||||
|
admin: ["M12 3l8 4v5c0 5.2-3.4 8.6-8 9.9C7.4 20.6 4 17.2 4 12V7l8-4z", "M9 12l2 2 4-4"],
|
||||||
|
chevronDown: ["M6 9l6 6 6-6"],
|
||||||
|
chevronLeft: ["M15 6l-6 6 6 6"],
|
||||||
|
chevronRight: ["M9 6l6 6-6 6"],
|
||||||
|
menu: ["M4 7h16", "M4 12h16", "M4 17h16"],
|
||||||
|
activity: [
|
||||||
|
"M12 3v3",
|
||||||
|
"M12 18v3",
|
||||||
|
"M4.9 4.9l2.1 2.1",
|
||||||
|
"M17 17l2.1 2.1",
|
||||||
|
"M3 12h3",
|
||||||
|
"M18 12h3",
|
||||||
|
"M4.9 19.1L7 17",
|
||||||
|
"M17 7l2.1-2.1",
|
||||||
|
"M12 15a3 3 0 100-6 3 3 0 000 6z",
|
||||||
|
],
|
||||||
|
layers: ["M12 4l8 4-8 4-8-4 8-4z", "M4 12l8 4 8-4", "M4 16l8 4 8-4"],
|
||||||
|
back: ["M19 12H5", "M12 5l-7 7 7 7"],
|
||||||
|
close: ["M18 6L6 18", "M6 6l12 12"],
|
||||||
|
list: ["M8 7h12", "M8 12h12", "M8 17h12", "M4 7h.01", "M4 12h.01", "M4 17h.01"],
|
||||||
|
clipboard: ["M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2", "M9 5a2 2 0 002 2h2a2 2 0 002-2", "M9 5a2 2 0 012-2h2a2 2 0 012 2"],
|
||||||
|
settings: ["M12 8.5a3.5 3.5 0 100 7 3.5 3.5 0 000-7z", "M19.4 15a1 1 0 00.2 1.1l.1.1a1 1 0 010 1.4l-1.1 1.1a1 1 0 01-1.4 0l-.1-.1a1 1 0 00-1.1-.2 1 1 0 00-.6.9V20a1 1 0 01-1 1h-1.6a1 1 0 01-1-1v-.2a1 1 0 00-.6-.9 1 1 0 00-1.1.2l-.1.1a1 1 0 01-1.4 0l-1.1-1.1a1 1 0 010-1.4l.1-.1a1 1 0 00.2-1.1 1 1 0 00-.9-.6H4a1 1 0 01-1-1v-1.6a1 1 0 011-1h.2a1 1 0 00.9-.6 1 1 0 00-.2-1.1l-.1-.1a1 1 0 010-1.4l1.1-1.1a1 1 0 011.4 0l.1.1a1 1 0 001.1.2 1 1 0 00.6-.9V4a1 1 0 011-1h1.6a1 1 0 011 1v.2a1 1 0 00.6.9 1 1 0 001.1-.2l.1-.1a1 1 0 011.4 0l1.1 1.1a1 1 0 010 1.4l-.1.1a1 1 0 00-.2 1.1 1 1 0 00.9.6H20a1 1 0 011 1v1.6a1 1 0 01-1 1h-.2a1 1 0 00-.9.6z"],
|
||||||
|
};
|
||||||
|
|
||||||
|
$: paths = iconPaths[name] || iconPaths.dashboard;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{#each paths as d}
|
||||||
|
<path d={d} />
|
||||||
|
{/each}
|
||||||
|
</svg>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<!-- Top Navigation Bar -->
|
<!-- Top Navigation Bar -->
|
||||||
<TopNavbar />
|
<TopNavbar />
|
||||||
<!-- Breadcrumbs -->
|
<!-- Breadcrumbs -->
|
||||||
<div class="mt-16">
|
<div class="mt-16 pt-3">
|
||||||
<Breadcrumbs />
|
<Breadcrumbs />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
18
frontend/static/favicon.svg
Normal file
18
frontend/static/favicon.svg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="10%" y1="0%" x2="90%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#0EA5E9" />
|
||||||
|
<stop offset="45%" stop-color="#06B6D4" />
|
||||||
|
<stop offset="100%" stop-color="#2563EB" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="stack" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#FFFFFF" />
|
||||||
|
<stop offset="100%" stop-color="#E2E8F0" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="4" y="4" width="56" height="56" rx="16" fill="url(#bg)" />
|
||||||
|
<path d="M32 16 14 24l18 8 18-8-18-8Z" fill="url(#stack)" opacity="0.98" />
|
||||||
|
<path d="m14 33 18 8 18-8" fill="none" stroke="#F8FAFC" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<path d="m14 42 18 8 18-8" fill="none" stroke="#F8FAFC" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" />
|
||||||
|
<path d="M49 14v6M46 17h6" stroke="#F8FAFC" stroke-width="2.2" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1013 B |
Reference in New Issue
Block a user