Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eaf6aceef1 | |||
| 876d9e0db9 | |||
| b40bd966a9 | |||
| 17348f46c8 | |||
| 9d2e8419be | |||
| cddc907859 | |||
| 61564296ed | |||
| 8fa78bf40f | |||
| 6cb99c0ab6 | |||
| 7b9ddb2dc5 | |||
| 00ded5d12e | |||
| 23fb25dcd5 | |||
| 8ddcb56f90 | |||
| 9d186ece0a | |||
| 55cb4880ce | |||
| 171aae7095 | |||
| 9aad9c0c2a | |||
| b91d3a9341 | |||
| 590e4bb039 | |||
| c4bef0d443 | |||
| 7f05b3b52d | |||
| 1fc1de6691 | |||
| 70c43ceae3 | |||
| cf247117d4 | |||
| 35ffde9253 | |||
| 531ec5a2d0 | |||
| cde6ccd841 | |||
| f181f662d8 | |||
| 85f26c69d6 | |||
| 9d1471a363 | |||
| 6db2e3ff4f | |||
| bbbc0958f4 | |||
| 52c9c7251e | |||
| a1409a4d57 | |||
| 61359bc712 | |||
| 889f84d666 | |||
| a188fcbe85 |
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"@git.zone/tswatch": {
|
||||||
|
"preset": "element"
|
||||||
|
},
|
||||||
|
"@git.zone/tsbundle": {
|
||||||
|
"bundles": [
|
||||||
|
{
|
||||||
|
"from": "./ts_web/index.ts",
|
||||||
|
"to": "./dist_bundle/bundle.js",
|
||||||
|
"outputMode": "bundle",
|
||||||
|
"bundler": "esbuild",
|
||||||
|
"production": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@git.zone/cli": {
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"projectType": "wcc",
|
||||||
|
"module": {
|
||||||
|
"githost": "code.foss.global",
|
||||||
|
"gitscope": "serve.zone",
|
||||||
|
"gitrepo": "catalog",
|
||||||
|
"description": "UI component catalog for serve.zone",
|
||||||
|
"npmPackagename": "@serve.zone/catalog",
|
||||||
|
"license": "MIT",
|
||||||
|
"projectDomain": "serve.zone"
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"preflight": {
|
||||||
|
"requireCleanTree": true,
|
||||||
|
"test": false,
|
||||||
|
"build": true
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"git": {
|
||||||
|
"enabled": true,
|
||||||
|
"remote": "origin",
|
||||||
|
"pushBranch": true,
|
||||||
|
"pushTags": true
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"enabled": true,
|
||||||
|
"registries": [
|
||||||
|
"https://verdaccio.lossless.digital",
|
||||||
|
"https://registry.npmjs.org"
|
||||||
|
],
|
||||||
|
"accessLevel": "public",
|
||||||
|
"alreadyPublished": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+108
-1
@@ -1,5 +1,112 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Pending
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-05-24 - 2.12.6
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- refresh reverse proxy UI labels and pnpm workspace config (catalog)
|
||||||
|
- Replaces stale reverse proxy references in dashboard and platform service examples with SmartProxy wording
|
||||||
|
- Generalizes the network proxy access log subtitle
|
||||||
|
- Bumps @types/node to ^25.8.0 and adds pnpm build dependency configuration
|
||||||
|
|
||||||
|
## 2026-05-24 - 2.12.5
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- pass dashboard resource usage into `sz-resource-usage-card` using the correct property
|
||||||
|
|
||||||
|
## 2026-04-13 - 2.12.4 - fix(deps)
|
||||||
|
bump @design.estate/dees-catalog to ^3.70.0
|
||||||
|
|
||||||
|
- Updates @design.estate/dees-catalog from ^3.67.1 to ^3.70.0 in package.json
|
||||||
|
|
||||||
|
## 2026-04-07 - 2.12.3 - fix(route-card)
|
||||||
|
align route card with dees-tile layout and update header and footer styling
|
||||||
|
|
||||||
|
- replace the custom card wrapper with dees-tile and move content into header, body, and footer slots
|
||||||
|
- restyle header, route name, and action buttons to match shared tile design tokens and interaction states
|
||||||
|
- wrap empty and populated route states consistently inside the tile component
|
||||||
|
|
||||||
|
## 2026-04-07 - 2.12.2 - fix(ts_web)
|
||||||
|
adjust route card section background color in dark theme
|
||||||
|
|
||||||
|
- Updates the dark theme background for route card sections from #0a0a0a to #101010 for improved visual consistency.
|
||||||
|
|
||||||
|
## 2026-04-07 - 2.12.1 - fix(ts_web)
|
||||||
|
handle slotted config section content visibility and field row borders correctly
|
||||||
|
|
||||||
|
- track whether the default slot has assigned content and hide the slot container when empty
|
||||||
|
- wrap rendered fields in a dedicated list so the last field row border is removed correctly when slot content is present
|
||||||
|
|
||||||
|
## 2026-04-07 - 2.12.0 - feat(elements)
|
||||||
|
standardize dashboard and detail views on dees tile and stats grid components
|
||||||
|
|
||||||
|
- replace custom stat cards with dees-statsgrid across status and network views and remove the obsolete sz-stat-card element
|
||||||
|
- migrate multiple detail and settings views to dees-tile headers and footers for consistent action placement and styling
|
||||||
|
- extend sz-config-section with footer links and actions plus improved collapsed tile behavior
|
||||||
|
|
||||||
|
## 2026-04-05 - 2.11.2 - fix(route-card)
|
||||||
|
align route card with source profile metadata and vpnOnly route configuration
|
||||||
|
|
||||||
|
- rename linked route metadata fields from security profile to source profile in rendering and feature detection
|
||||||
|
- simplify VPN display logic to use the boolean vpnOnly flag instead of the previous nested VPN configuration object
|
||||||
|
|
||||||
|
## 2026-04-04 - 2.11.1 - fix(route-card)
|
||||||
|
clarify VPN mode badge labels in route cards
|
||||||
|
|
||||||
|
- Renames VPN badge text from "VPN Only"/"VPN + Public" to "VPN Mandatory"/"VPN Voluntary" for clearer route mode descriptions.
|
||||||
|
|
||||||
|
## 2026-04-02 - 2.11.0 - feat(route-ui)
|
||||||
|
add VPN details and conditional card actions to route cards
|
||||||
|
|
||||||
|
- Extend route card data and rendering to display VPN access mode and allowed client tags.
|
||||||
|
- Add optional Edit and Delete action buttons that emit route-edit and route-delete events.
|
||||||
|
- Allow the route list view to control action visibility per route via a showActionsFilter callback.
|
||||||
|
- Include VPN as a visible route feature indicator in the card summary.
|
||||||
|
|
||||||
|
## 2026-04-02 - 2.10.0 - feat(docs)
|
||||||
|
document newly available catalog components and updated build configuration details
|
||||||
|
|
||||||
|
- Update component counts and add documentation for App Store, Routes, MTA/Email, and Configuration views
|
||||||
|
- Expand the README with new component tables and exported TypeScript types
|
||||||
|
- Refresh build notes to reference .smartconfig.json and the renamed config file
|
||||||
|
|
||||||
|
## 2026-04-02 - 2.9.1 - fix(build)
|
||||||
|
migrate build configuration to .smartconfig and update toolchain dependencies
|
||||||
|
|
||||||
|
- replace npmextra.json with .smartconfig.json and add tsbundle bundle configuration
|
||||||
|
- simplify the build script to use the centralized tsbundle configuration
|
||||||
|
- update build and UI package dependencies and switch TypeScript to explicit node types
|
||||||
|
|
||||||
|
## 2026-03-19 - 2.9.0 - feat(app-store-view)
|
||||||
|
add app details action to store cards
|
||||||
|
|
||||||
|
- adds a new "View Details" button alongside the existing deploy action
|
||||||
|
- dispatches a bubbling "view-app" event with the selected app payload for parent handlers
|
||||||
|
- updates card action layout and button styling to support multiple actions
|
||||||
|
|
||||||
|
## 2026-03-18 - 2.8.0 - feat(elements)
|
||||||
|
add app store view component for browsing and deploying app templates
|
||||||
|
|
||||||
|
- introduces a new sz-app-store-view element with app card rendering, search/filter empty states, and deploy action events
|
||||||
|
- exports the new app store view from the elements index for public consumption
|
||||||
|
|
||||||
|
## 2026-03-17 - 2.7.0 - feat(sz-service-detail-view)
|
||||||
|
replace the custom logs panel with dees-chart-log in the service detail view
|
||||||
|
|
||||||
|
- Removes the bespoke log streaming UI styles and markup in favor of a shared log chart component.
|
||||||
|
- Maps service log entries to structured timestamp, level, and message data for the new component.
|
||||||
|
- Enables auto-scrolling, metrics display, and a higher log entry limit in the embedded log viewer.
|
||||||
|
|
||||||
|
## 2026-03-16 - 2.6.2 - fix(platform-service-detail-view)
|
||||||
|
wrap service logs chart in a full-width container to preserve layout
|
||||||
|
|
||||||
|
- Places the logs component inside a container spanning all grid columns.
|
||||||
|
- Keeps the service logs view aligned correctly within the detail page layout.
|
||||||
|
|
||||||
## 2026-03-16 - 2.6.1 - fix(platform-service-detail-view)
|
## 2026-03-16 - 2.6.1 - fix(platform-service-detail-view)
|
||||||
replace custom service log markup with dees-chart-log in the platform service detail view
|
replace custom service log markup with dees-chart-log in the platform service detail view
|
||||||
|
|
||||||
@@ -91,4 +198,4 @@ bump dependencies and devDependencies, update watch script, add npmextra preset,
|
|||||||
General maintenance update; the commit message provides no detailed information.
|
General maintenance update; the commit message provides no detailed information.
|
||||||
|
|
||||||
- Commit message: "update" (no further details).
|
- Commit message: "update" (no further details).
|
||||||
- No specific user-facing or functional changes are documented.
|
- No specific user-facing or functional changes are documented.
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Task Venture Capital GmbH
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"@git.zone/tswatch": {
|
|
||||||
"preset": "element"
|
|
||||||
},
|
|
||||||
"@git.zone/cli": {
|
|
||||||
"projectType": "wcc",
|
|
||||||
"module": {
|
|
||||||
"githost": "code.foss.global",
|
|
||||||
"gitscope": "serve.zone",
|
|
||||||
"gitrepo": "catalog",
|
|
||||||
"description": "UI component catalog for serve.zone",
|
|
||||||
"npmPackagename": "@serve.zone/catalog",
|
|
||||||
"license": "MIT",
|
|
||||||
"projectDomain": "serve.zone"
|
|
||||||
},
|
|
||||||
"release": {
|
|
||||||
"registries": [
|
|
||||||
"https://verdaccio.lossless.digital",
|
|
||||||
"https://registry.npmjs.org"
|
|
||||||
],
|
|
||||||
"accessLevel": "public"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+15
-16
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/catalog",
|
"name": "@serve.zone/catalog",
|
||||||
"version": "2.6.1",
|
"version": "2.12.6",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "UI component catalog for serve.zone",
|
"description": "UI component catalog for serve.zone",
|
||||||
"main": "dist_ts_web/index.js",
|
"main": "dist_ts_web/index.js",
|
||||||
@@ -8,35 +8,34 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "tstest test/",
|
"test": "tstest test/",
|
||||||
"build": "tsbuild tsfolders --allowimplicitany && tsbundle element --production",
|
"build": "tsbuild tsfolders --allowimplicitany && tsbundle",
|
||||||
"watch": "tswatch"
|
"watch": "tswatch"
|
||||||
},
|
},
|
||||||
"author": "Lossless GmbH",
|
"author": "Task Venture Capital GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@design.estate/dees-catalog": "^3.43.0",
|
"@design.estate/dees-catalog": "^3.81.0",
|
||||||
"@design.estate/dees-domtools": "^2.3.8",
|
"@design.estate/dees-domtools": "^2.5.6",
|
||||||
"@design.estate/dees-element": "^2.1.6",
|
"@design.estate/dees-element": "^2.2.4",
|
||||||
"@design.estate/dees-wcctools": "^3.8.0"
|
"@design.estate/dees-wcctools": "^3.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^4.1.2",
|
"@git.zone/tsbuild": "^4.4.2",
|
||||||
"@git.zone/tsbundle": "^2.8.3",
|
"@git.zone/tsbundle": "^2.10.4",
|
||||||
"@git.zone/tsrun": "^2.0.1",
|
"@git.zone/tsrun": "^2.0.4",
|
||||||
"@git.zone/tstest": "^3.1.8",
|
"@git.zone/tstest": "^3.6.6",
|
||||||
"@git.zone/tswatch": "^3.1.0",
|
"@git.zone/tswatch": "^3.3.5",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.1.0",
|
||||||
"@types/node": "^25.3.0"
|
"@types/node": "^25.8.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
|
||||||
"ts_web/**/*",
|
"ts_web/**/*",
|
||||||
"dist/**/*",
|
"dist/**/*",
|
||||||
"dist_*/**/*",
|
"dist_*/**/*",
|
||||||
"dist_ts/**/*",
|
"dist_ts/**/*",
|
||||||
"dist_ts_web/**/*",
|
"dist_ts_web/**/*",
|
||||||
"assets/**/*",
|
"assets/**/*",
|
||||||
"npmextra.json",
|
".smartconfig.json",
|
||||||
"readme.md"
|
"readme.md"
|
||||||
],
|
],
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
|||||||
Generated
+3061
-3692
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
|||||||
|
allowBuilds:
|
||||||
|
esbuild: true
|
||||||
|
mongodb-memory-server: true
|
||||||
|
puppeteer: true
|
||||||
|
sharp: true
|
||||||
|
ignoredBuiltDependencies:
|
||||||
|
- '@design.estate/dees-catalog'
|
||||||
+8
-3
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
- `html/index.ts` - WccTools setup with sections for Pages and Elements
|
- `html/index.ts` - WccTools setup with sections for Pages and Elements
|
||||||
- `ts_web/elements/` - All web components (27 elements + 6 demo-view wrappers)
|
- `ts_web/elements/` - All web components (33 elements + 9 demo-view wrappers)
|
||||||
- `ts_web/elements/index.ts` - Barrel export for all element components
|
- `ts_web/elements/index.ts` - Barrel export for all element components
|
||||||
- `ts_web/pages/` - Page components
|
- `ts_web/pages/` - Page components
|
||||||
|
|
||||||
@@ -16,13 +16,18 @@
|
|||||||
## Demo Groups
|
## Demo Groups
|
||||||
| Group | Elements |
|
| Group | Elements |
|
||||||
|-------|----------|
|
|-------|----------|
|
||||||
| Dashboard | sz-dashboard-view, sz-stat-card, sz-resource-usage-card, sz-traffic-card, sz-quick-actions-card |
|
| Dashboard | sz-dashboard-view, sz-resource-usage-card, sz-traffic-card, sz-quick-actions-card |
|
||||||
| Dashboard Grids | sz-status-grid-cluster, sz-status-grid-services, sz-status-grid-network, sz-status-grid-infra |
|
| Dashboard Grids | sz-status-grid-cluster, sz-status-grid-services, sz-status-grid-network, sz-status-grid-infra |
|
||||||
| Platform | sz-platform-services-card, sz-platform-service-detail-view |
|
| Platform | sz-platform-services-card, sz-platform-service-detail-view |
|
||||||
| Network | sz-network-proxy-view, sz-network-dns-view, sz-network-domains-view, sz-reverse-proxy-card, sz-dns-ssl-card, sz-certificates-card, sz-domain-detail-view |
|
| Network | sz-network-proxy-view, sz-network-dns-view, sz-network-domains-view, sz-reverse-proxy-card, sz-dns-ssl-card, sz-certificates-card, sz-domain-detail-view |
|
||||||
|
| Routes | sz-route-list-view, sz-route-card |
|
||||||
| Services | sz-services-list-view, sz-services-backups-view, sz-service-detail-view, sz-service-create-view |
|
| Services | sz-services-list-view, sz-services-backups-view, sz-service-detail-view, sz-service-create-view |
|
||||||
|
| App Store | sz-app-store-view |
|
||||||
|
| MTA / Email | sz-mta-list-view, sz-mta-detail-view |
|
||||||
|
| Configuration | sz-config-overview, sz-config-section |
|
||||||
| Auth & Settings | sz-login-view, sz-tokens-view, sz-settings-view, sz-registry-advertisement, sz-registry-external-view |
|
| Auth & Settings | sz-login-view, sz-tokens-view, sz-settings-view, sz-registry-advertisement, sz-registry-external-view |
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
- `pnpm run build` - tsbuild tsfolders + tsbundle element --production
|
- `pnpm run build` - tsbuild tsfolders + tsbundle (reads from .smartconfig.json)
|
||||||
- `pnpm run watch` - starts wcctools dev server
|
- `pnpm run watch` - starts wcctools dev server
|
||||||
|
- Config file: `.smartconfig.json` (renamed from npmextra.json)
|
||||||
|
|||||||
@@ -1,216 +1,159 @@
|
|||||||
# @serve.zone/catalog
|
# @serve.zone/catalog
|
||||||
|
|
||||||
The complete UI component library for **serve.zone** — a full-featured admin and management interface for onebox server management, built as a collection of reusable web components.
|
`@serve.zone/catalog` is the shared web-component catalog for serve.zone dashboards. It packages the UI building blocks used by Onebox, DcRouter-style operations views, and other serve.zone admin surfaces: dashboards, service views, app-store screens, DNS and routing tools, registry panels, email views, settings, tokens, and demo shells.
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm install @serve.zone/catalog
|
|
||||||
```
|
|
||||||
|
|
||||||
## Issue Reporting and Security
|
## Issue Reporting and Security
|
||||||
|
|
||||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||||
|
|
||||||
## 🚀 What It Does
|
## Install
|
||||||
|
|
||||||
`@serve.zone/catalog` provides **30+ production-ready web components** covering every aspect of server management:
|
```bash
|
||||||
|
pnpm add @serve.zone/catalog
|
||||||
- 📊 **Dashboard** — Real-time cluster overview, resource usage, traffic metrics, quick actions
|
|
||||||
- 🐳 **Services** — Docker container management, deployment, logs, live stats, backups, and an integrated IDE workspace
|
|
||||||
- 🌐 **Network** — Reverse proxy configuration, DNS record management, domain & SSL certificate monitoring
|
|
||||||
- 📦 **Registries** — Container registry management (onebox + external registries like Docker Hub, GHCR, ECR)
|
|
||||||
- 🔑 **Auth** — Login view, API token management (global + CI tokens)
|
|
||||||
- ⚙️ **Settings** — Appearance, Cloudflare integration, SSL/TLS config, network settings, account management
|
|
||||||
- 🏗️ **Platform Services** — MongoDB, MinIO, ClickHouse, Redis, Caddy monitoring and control
|
|
||||||
|
|
||||||
Every component supports **light and dark themes** out of the box and communicates via standard `CustomEvent` dispatching.
|
|
||||||
|
|
||||||
## 📦 Usage
|
|
||||||
|
|
||||||
### Import
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Import everything
|
|
||||||
import * as szCatalog from '@serve.zone/catalog';
|
|
||||||
|
|
||||||
// Or import specific components
|
|
||||||
import { SzDashboardView, SzLoginView, SzServiceDetailView } from '@serve.zone/catalog';
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Components auto-register as custom elements when imported. Use them directly in your HTML:
|
## What It Provides
|
||||||
|
|
||||||
|
The package exports browser-first TypeScript modules from `ts_web/index.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export * from './elements/index.js';
|
||||||
|
export * from './pages/index.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
Importing a component registers its custom element through `@design.estate/dees-element` decorators. Components use the Dees design system, support light and dark themes through Dees theme helpers, and communicate through DOM properties plus `CustomEvent` dispatching.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
SzDashboardView,
|
||||||
|
SzLoginView,
|
||||||
|
SzServiceDetailView,
|
||||||
|
} from '@serve.zone/catalog';
|
||||||
|
```
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<sz-dashboard-view .data="${dashboardData}"></sz-dashboard-view>
|
<sz-dashboard-view></sz-dashboard-view>
|
||||||
<sz-login-view @login="${handleLogin}"></sz-login-view>
|
<sz-login-view></sz-login-view>
|
||||||
<sz-service-detail-view .service="${serviceData}" .logs="${logEntries}"></sz-service-detail-view>
|
<sz-service-detail-view></sz-service-detail-view>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Full Application Shell
|
## Component Areas
|
||||||
|
|
||||||
For a complete app experience, use the demo app shell which wires up sidebar navigation, app bar menus, and all views via `dees-appui`:
|
| Area | Components |
|
||||||
|
| --- | --- |
|
||||||
|
| Dashboard | `SzDashboardView`, `SzResourceUsageCard`, `SzTrafficCard`, `SzPlatformServicesCard`, `SzCertificatesCard`, `SzReverseProxyCard`, `SzDnsSslCard`, `SzQuickActionsCard`, and status-grid components. |
|
||||||
|
| Services | `SzServicesListView`, `SzServiceDetailView`, `SzServiceCreateView`, `SzServicesBackupsView`, and `SzAppStoreView`. |
|
||||||
|
| Network and domains | `SzNetworkProxyView`, `SzNetworkDnsView`, `SzNetworkDomainsView`, and `SzDomainDetailView`. |
|
||||||
|
| Routes | `SzRouteListView` and `SzRouteCard` for route configuration, match criteria, actions, TLS mode, and security profile display. |
|
||||||
|
| Mail | `SzMtaListView` and `SzMtaDetailView` for inbound/outbound email operations views. |
|
||||||
|
| Registries | `SzRegistryAdvertisement` and `SzRegistryExternalView`. |
|
||||||
|
| Auth and settings | `SzLoginView`, `SzTokensView`, and `SzSettingsView`. |
|
||||||
|
| Configuration | `SzConfigOverview` and `SzConfigSection`. |
|
||||||
|
| Demo pages | `SzDemoAppShell`, `SzDemoApp`, `Mainpage`, and `SzDemoView*` components used by the development/demo environment. |
|
||||||
|
|
||||||
|
The exported catalog currently contains more than 30 product UI components plus demo orchestration components. Prefer the exported barrel imports over deep file imports unless you are working inside this repository.
|
||||||
|
|
||||||
|
## Usage Patterns
|
||||||
|
|
||||||
|
Most components expose data through typed properties and emit actions as DOM events.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
import { html } from '@design.estate/dees-element';
|
||||||
import '@serve.zone/catalog';
|
import '@serve.zone/catalog';
|
||||||
// Then use <sz-demo-app-shell> for a fully configured application
|
|
||||||
|
const dashboardTemplate = html`
|
||||||
|
<sz-dashboard-view
|
||||||
|
.data=${dashboardData}
|
||||||
|
@action-click=${(event: CustomEvent) => {
|
||||||
|
console.log('dashboard action selected', event.detail);
|
||||||
|
}}
|
||||||
|
></sz-dashboard-view>
|
||||||
|
`;
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🧩 Component Reference
|
For a full app-style demo shell with navigation, import the package and render `<sz-demo-app-shell>`:
|
||||||
|
|
||||||
### Dashboard
|
```html
|
||||||
|
<sz-demo-app-shell></sz-demo-app-shell>
|
||||||
|
```
|
||||||
|
|
||||||
| Component | Tag | Description |
|
The demo shell is useful for development and visual review. Production apps usually compose the lower-level components into their own routing and data-fetching layer.
|
||||||
|-----------|-----|-------------|
|
|
||||||
| `SzDashboardView` | `<sz-dashboard-view>` | Main dashboard orchestrating all grid sections — cluster, services, network, infrastructure |
|
|
||||||
| `SzStatCard` | `<sz-stat-card>` | Single statistic card with label, value, icon, and color variant |
|
|
||||||
| `SzResourceUsageCard` | `<sz-resource-usage-card>` | CPU/memory progress bars, network I/O, top memory consumers |
|
|
||||||
| `SzTrafficCard` | `<sz-traffic-card>` | HTTP traffic metrics — requests, errors, response time, status distribution |
|
|
||||||
| `SzQuickActionsCard` | `<sz-quick-actions-card>` | Configurable action button grid |
|
|
||||||
|
|
||||||
### Dashboard Grids
|
## TypeScript Interfaces
|
||||||
|
|
||||||
| Component | Tag | Description |
|
Many components export their own data interfaces alongside the custom element class. These interfaces are intended for UI-facing view models, not as replacements for backend contracts from `@serve.zone/interfaces`.
|
||||||
|-----------|-----|-------------|
|
|
||||||
| `SzStatusGridCluster` | `<sz-status-grid-cluster>` | 4-column stat card grid — total/running/stopped services, Docker status |
|
|
||||||
| `SzStatusGridServices` | `<sz-status-grid-services>` | Resource usage + platform services side by side |
|
|
||||||
| `SzStatusGridNetwork` | `<sz-status-grid-network>` | Traffic, reverse proxy, and certificates in a responsive grid |
|
|
||||||
| `SzStatusGridInfra` | `<sz-status-grid-infra>` | DNS/SSL status + quick actions |
|
|
||||||
|
|
||||||
### Services
|
|
||||||
|
|
||||||
| Component | Tag | Description |
|
|
||||||
|-----------|-----|-------------|
|
|
||||||
| `SzServicesListView` | `<sz-services-list-view>` | Table of deployed services with status badges and action buttons |
|
|
||||||
| `SzServiceDetailView` | `<sz-service-detail-view>` | Full service detail page — info, logs, live stats, actions, backups, and integrated workspace/IDE mode |
|
|
||||||
| `SzServiceCreateView` | `<sz-service-create-view>` | Service deployment form — image, ports, env vars, volumes, resource limits |
|
|
||||||
| `SzServicesBackupsView` | `<sz-services-backups-view>` | Backup schedule and backup history management |
|
|
||||||
|
|
||||||
### Platform Services
|
|
||||||
|
|
||||||
| Component | Tag | Description |
|
|
||||||
|-----------|-----|-------------|
|
|
||||||
| `SzPlatformServicesCard` | `<sz-platform-services-card>` | Lists infrastructure services (MongoDB, MinIO, etc.) with status indicators |
|
|
||||||
| `SzPlatformServiceDetailView` | `<sz-platform-service-detail-view>` | Detailed platform service view — connection info, config, metrics, logs |
|
|
||||||
|
|
||||||
### Network
|
|
||||||
|
|
||||||
| Component | Tag | Description |
|
|
||||||
|-----------|-----|-------------|
|
|
||||||
| `SzNetworkProxyView` | `<sz-network-proxy-view>` | Reverse proxy management — traffic targets table and access log viewer |
|
|
||||||
| `SzNetworkDnsView` | `<sz-network-dns-view>` | DNS record management with Cloudflare sync |
|
|
||||||
| `SzNetworkDomainsView` | `<sz-network-domains-view>` | Domain list with certificate status and provider info |
|
|
||||||
| `SzDomainDetailView` | `<sz-domain-detail-view>` | Domain detail — SSL certificate info, proxy routes, DNS records |
|
|
||||||
| `SzReverseProxyCard` | `<sz-reverse-proxy-card>` | Compact proxy status card (HTTP/HTTPS ports, route count) |
|
|
||||||
| `SzDnsSslCard` | `<sz-dns-ssl-card>` | Cloudflare DNS and ACME config status |
|
|
||||||
| `SzCertificatesCard` | `<sz-certificates-card>` | Certificate status counts — valid, expiring, expired |
|
|
||||||
|
|
||||||
### Registries
|
|
||||||
|
|
||||||
| Component | Tag | Description |
|
|
||||||
|-----------|-----|-------------|
|
|
||||||
| `SzRegistryAdvertisement` | `<sz-registry-advertisement>` | Onebox registry info card with docker quick-start commands |
|
|
||||||
| `SzRegistryExternalView` | `<sz-registry-external-view>` | External registry management (Docker Hub, GHCR, GCR, ECR) |
|
|
||||||
|
|
||||||
### Auth & Settings
|
|
||||||
|
|
||||||
| Component | Tag | Description |
|
|
||||||
|-----------|-----|-------------|
|
|
||||||
| `SzLoginView` | `<sz-login-view>` | Login page with serve.zone branding, credentials form, error display |
|
|
||||||
| `SzTokensView` | `<sz-tokens-view>` | API token management — global and CI tokens with copy/regenerate/delete |
|
|
||||||
| `SzSettingsView` | `<sz-settings-view>` | Full settings panel — appearance, Cloudflare, SSL/TLS, network, account |
|
|
||||||
|
|
||||||
## 🏗️ Architecture
|
|
||||||
|
|
||||||
### Component Pattern
|
|
||||||
|
|
||||||
All components follow a consistent pattern:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { DeesElement, customElement, html, css, cssManager, property } from '@design.estate/dees-element';
|
import type {
|
||||||
|
IDashboardData,
|
||||||
|
IRouteConfig,
|
||||||
|
IServiceDetail,
|
||||||
|
IAppTemplate,
|
||||||
|
} from '@serve.zone/catalog';
|
||||||
|
```
|
||||||
|
|
||||||
@customElement('sz-my-component')
|
Use these types to shape component props in the frontend. Use `@serve.zone/interfaces` for API and persisted platform contracts.
|
||||||
export class SzMyComponent extends DeesElement {
|
|
||||||
// TC39 standard decorators with accessor keyword
|
## Architecture
|
||||||
|
|
||||||
|
The catalog follows a consistent component pattern:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
property,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
@customElement('sz-example-card')
|
||||||
|
export class SzExampleCard extends DeesElement {
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
public accessor label: string = '';
|
public accessor label = '';
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
css`
|
css`
|
||||||
/* Light/dark theme support */
|
:host {
|
||||||
:host { color: ${cssManager.bdTheme('#18181b', '#fafafa')}; }
|
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Events via CustomEvent (bubbles + composed for shadow DOM)
|
public render() {
|
||||||
private handleClick() {
|
return html`<span>${this.label}</span>`;
|
||||||
this.dispatchEvent(new CustomEvent('action', {
|
|
||||||
detail: { id: this.id },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Project Structure
|
Project layout:
|
||||||
|
|
||||||
```
|
```text
|
||||||
@serve.zone/catalog/
|
@serve.zone/catalog/
|
||||||
├── html/ # WccTools dev server entry point
|
├── html/ # WccTools dev server shell
|
||||||
│ ├── index.html # HTML shell
|
|
||||||
│ └── index.ts # WccTools config (pages + elements sections)
|
|
||||||
├── ts_web/
|
├── ts_web/
|
||||||
│ ├── index.ts # Barrel export
|
│ ├── index.ts # package barrel export
|
||||||
│ ├── elements/ # All web components
|
│ ├── elements/ # reusable sz-* web components
|
||||||
│ │ ├── index.ts # Element barrel export
|
│ └── pages/ # demo app and shell pages
|
||||||
│ │ ├── sz-*.ts # Individual components
|
└── dist_ts_web/ # compiled npm entry point
|
||||||
│ │ └── sz-demo-view-*.ts # Demo orchestration wrappers
|
|
||||||
│ └── pages/ # Page-level components
|
|
||||||
│ ├── sz-demo-app-shell.ts # Full app shell (dees-appui)
|
|
||||||
│ └── ...
|
|
||||||
└── dist_ts_web/ # Compiled output (npm entry point)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### TypeScript Interfaces
|
## Development
|
||||||
|
|
||||||
The library exports comprehensive TypeScript interfaces for all data structures:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Dashboard
|
|
||||||
import type { IDashboardData, IResourceUsage, ITrafficData, IClusterStats } from '@serve.zone/catalog';
|
|
||||||
|
|
||||||
// Services
|
|
||||||
import type { IServiceDetail, IServiceStats, ILogEntry, IServiceBackup } from '@serve.zone/catalog';
|
|
||||||
|
|
||||||
// Network
|
|
||||||
import type { IDomainDetail, ICertificateDetail, IDnsRecord, ITrafficTarget } from '@serve.zone/catalog';
|
|
||||||
|
|
||||||
// Settings & Auth
|
|
||||||
import type { ISettings, IToken, IExternalRegistry } from '@serve.zone/catalog';
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🛠️ Development
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
|
||||||
pnpm install
|
pnpm install
|
||||||
|
|
||||||
# Start dev server (wcctools dashboard with live reload)
|
|
||||||
pnpm run watch
|
pnpm run watch
|
||||||
|
|
||||||
# Production build
|
|
||||||
pnpm run build
|
pnpm run build
|
||||||
|
|
||||||
# Run tests
|
|
||||||
pnpm test
|
pnpm test
|
||||||
```
|
```
|
||||||
|
|
||||||
The **wcctools dev server** provides an interactive dashboard where every component is rendered with demo data. Components are organized by group (Dashboard, Services, Network, etc.) in the sidebar. Demo view wrappers (`sz-demo-view-*`) are filtered out from the element list — they serve as full-page orchestration demos accessible through the Pages section.
|
`pnpm run watch` starts the WccTools development environment for browsing the catalog with demo data. `pnpm run build` compiles TypeScript folders and creates the browser bundle with `tsbundle`.
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
|
||||||
|
|
||||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/catalog',
|
name: '@serve.zone/catalog',
|
||||||
version: '2.6.1',
|
version: '2.12.6',
|
||||||
description: 'UI component catalog for serve.zone'
|
description: 'UI component catalog for serve.zone'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// Dashboard Cards
|
// Dashboard Cards
|
||||||
export * from './sz-stat-card.js';
|
|
||||||
export * from './sz-resource-usage-card.js';
|
export * from './sz-resource-usage-card.js';
|
||||||
export * from './sz-traffic-card.js';
|
export * from './sz-traffic-card.js';
|
||||||
export * from './sz-platform-services-card.js';
|
export * from './sz-platform-services-card.js';
|
||||||
@@ -28,6 +27,7 @@ export * from './sz-registry-external-view.js';
|
|||||||
export * from './sz-services-list-view.js';
|
export * from './sz-services-list-view.js';
|
||||||
export * from './sz-services-backups-view.js';
|
export * from './sz-services-backups-view.js';
|
||||||
export * from './sz-service-detail-view.js';
|
export * from './sz-service-detail-view.js';
|
||||||
|
export * from './sz-app-store-view.js';
|
||||||
|
|
||||||
// Tokens View
|
// Tokens View
|
||||||
export * from './sz-tokens-view.js';
|
export * from './sz-tokens-view.js';
|
||||||
|
|||||||
@@ -0,0 +1,611 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
property,
|
||||||
|
state,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sz-app-store-view': SzAppStoreView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAppTemplate {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
category: string;
|
||||||
|
iconUrl?: string;
|
||||||
|
iconName?: string;
|
||||||
|
image: string;
|
||||||
|
port: number;
|
||||||
|
envVars?: Array<{ key: string; value: string; description: string; required?: boolean }>;
|
||||||
|
volumes?: string[];
|
||||||
|
enableMongoDB?: boolean;
|
||||||
|
enableS3?: boolean;
|
||||||
|
enableClickHouse?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('sz-app-store-view')
|
||||||
|
export class SzAppStoreView extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<div style="padding: 24px; max-width: 1200px;">
|
||||||
|
<sz-app-store-view
|
||||||
|
.apps=${[
|
||||||
|
{
|
||||||
|
id: 'wordpress',
|
||||||
|
name: 'WordPress',
|
||||||
|
description: 'The world\'s most popular open-source content management system for building websites and blogs.',
|
||||||
|
category: 'CMS',
|
||||||
|
iconName: 'file-text',
|
||||||
|
image: 'wordpress:latest',
|
||||||
|
port: 80,
|
||||||
|
envVars: [
|
||||||
|
{ key: 'WORDPRESS_DB_HOST', value: '', description: 'Database host address', required: true },
|
||||||
|
{ key: 'WORDPRESS_DB_USER', value: 'wordpress', description: 'Database username', required: true },
|
||||||
|
{ key: 'WORDPRESS_DB_PASSWORD', value: '', description: 'Database password', required: true },
|
||||||
|
{ key: 'WORDPRESS_DB_NAME', value: 'wordpress', description: 'Database name', required: true },
|
||||||
|
],
|
||||||
|
volumes: ['/var/www/html'],
|
||||||
|
enableMongoDB: false,
|
||||||
|
enableS3: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gitea',
|
||||||
|
name: 'Gitea',
|
||||||
|
description: 'A lightweight, self-hosted Git service. Painless setup for your own code hosting platform.',
|
||||||
|
category: 'Development',
|
||||||
|
iconName: 'git-branch',
|
||||||
|
image: 'gitea/gitea:latest',
|
||||||
|
port: 3000,
|
||||||
|
envVars: [
|
||||||
|
{ key: 'GITEA__database__DB_TYPE', value: 'sqlite3', description: 'Database type', required: true },
|
||||||
|
{ key: 'GITEA__server__ROOT_URL', value: '', description: 'Public URL of the Gitea instance', required: false },
|
||||||
|
],
|
||||||
|
volumes: ['/data'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ghost',
|
||||||
|
name: 'Ghost',
|
||||||
|
description: 'A powerful open-source publishing platform for professional bloggers and content creators.',
|
||||||
|
category: 'CMS',
|
||||||
|
iconName: 'book-open',
|
||||||
|
image: 'ghost:latest',
|
||||||
|
port: 2368,
|
||||||
|
envVars: [
|
||||||
|
{ key: 'url', value: '', description: 'Public URL for the Ghost site', required: true },
|
||||||
|
{ key: 'database__client', value: 'sqlite3', description: 'Database client type', required: false },
|
||||||
|
],
|
||||||
|
volumes: ['/var/lib/ghost/content'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'nginx',
|
||||||
|
name: 'Nginx',
|
||||||
|
description: 'High-performance HTTP server and reverse proxy with low resource consumption.',
|
||||||
|
category: 'Web Server',
|
||||||
|
iconName: 'globe',
|
||||||
|
image: 'nginx:alpine',
|
||||||
|
port: 80,
|
||||||
|
volumes: ['/usr/share/nginx/html', '/etc/nginx/conf.d'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'redis',
|
||||||
|
name: 'Redis',
|
||||||
|
description: 'In-memory data store used as a database, cache, streaming engine, and message broker.',
|
||||||
|
category: 'Database',
|
||||||
|
iconName: 'database',
|
||||||
|
image: 'redis:alpine',
|
||||||
|
port: 6379,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'postgres',
|
||||||
|
name: 'PostgreSQL',
|
||||||
|
description: 'Advanced open-source relational database with strong reliability and feature set.',
|
||||||
|
category: 'Database',
|
||||||
|
iconName: 'database',
|
||||||
|
image: 'postgres:16-alpine',
|
||||||
|
port: 5432,
|
||||||
|
envVars: [
|
||||||
|
{ key: 'POSTGRES_USER', value: 'postgres', description: 'Superuser username', required: true },
|
||||||
|
{ key: 'POSTGRES_PASSWORD', value: '', description: 'Superuser password', required: true },
|
||||||
|
{ key: 'POSTGRES_DB', value: 'postgres', description: 'Default database name', required: false },
|
||||||
|
],
|
||||||
|
volumes: ['/var/lib/postgresql/data'],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
></sz-app-store-view>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static demoGroups = ['Services'];
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor apps: IAppTemplate[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor selectedCategory: string = 'All';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor searchQuery: string = '';
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-pills {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-pill {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
||||||
|
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||||
|
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-pill:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
|
||||||
|
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-pill.active {
|
||||||
|
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
||||||
|
color: ${cssManager.bdTheme('#fafafa', '#18181b')};
|
||||||
|
border-color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 240px;
|
||||||
|
padding: 8px 12px 8px 36px;
|
||||||
|
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 200ms ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input::placeholder {
|
||||||
|
color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-wrapper {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.app-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.app-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-card {
|
||||||
|
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-card:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
|
||||||
|
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.05)', 'rgba(0,0,0,0.2)')};
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon svg {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon .letter-fallback {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 4px;
|
||||||
|
background: ${cssManager.bdTheme('#eff6ff', 'rgba(59, 130, 246, 0.15)')};
|
||||||
|
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-description {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid ${cssManager.bdTheme('#f4f4f5', '#1a1a1e')};
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-tag {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deploy-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 7px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
border: none;
|
||||||
|
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
||||||
|
color: ${cssManager.bdTheme('#fafafa', '#18181b')};
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deploy-button:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deploy-button svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 7px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
||||||
|
background: transparent;
|
||||||
|
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-button:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
|
||||||
|
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
padding: 64px 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
private get categories(): string[] {
|
||||||
|
const cats = new Set(this.apps.map((app) => app.category));
|
||||||
|
return ['All', ...Array.from(cats).sort()];
|
||||||
|
}
|
||||||
|
|
||||||
|
private get filteredApps(): IAppTemplate[] {
|
||||||
|
let result = this.apps;
|
||||||
|
|
||||||
|
if (this.selectedCategory !== 'All') {
|
||||||
|
result = result.filter((app) => app.category === this.selectedCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.searchQuery.trim()) {
|
||||||
|
const query = this.searchQuery.trim().toLowerCase();
|
||||||
|
result = result.filter(
|
||||||
|
(app) =>
|
||||||
|
app.name.toLowerCase().includes(query) ||
|
||||||
|
app.description.toLowerCase().includes(query) ||
|
||||||
|
app.category.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const filtered = this.filteredApps;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-title">App Store</div>
|
||||||
|
<div class="header-subtitle">Deploy popular applications with one click</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-bar">
|
||||||
|
<div class="category-pills">
|
||||||
|
${this.categories.map(
|
||||||
|
(cat) => html`
|
||||||
|
<button
|
||||||
|
class="category-pill ${this.selectedCategory === cat ? 'active' : ''}"
|
||||||
|
@click=${() => { this.selectedCategory = cat; }}
|
||||||
|
>
|
||||||
|
${cat}
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="search-wrapper">
|
||||||
|
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="Search apps..."
|
||||||
|
.value=${this.searchQuery}
|
||||||
|
@input=${(e: Event) => { this.searchQuery = (e.target as HTMLInputElement).value; }}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="app-grid">
|
||||||
|
${filtered.length > 0
|
||||||
|
? filtered.map((app) => this.renderAppCard(app))
|
||||||
|
: html`
|
||||||
|
<div class="empty-state">
|
||||||
|
<svg class="empty-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
<line x1="8" y1="11" x2="14" y2="11"></line>
|
||||||
|
</svg>
|
||||||
|
<div class="empty-title">No apps found</div>
|
||||||
|
<div class="empty-description">
|
||||||
|
Try adjusting your search or filter to find what you're looking for.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderAppCard(app: IAppTemplate): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="app-card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="app-icon">
|
||||||
|
${app.iconUrl
|
||||||
|
? html`<img src="${app.iconUrl}" alt="${app.name}">`
|
||||||
|
: app.iconName
|
||||||
|
? this.renderIconByName(app.iconName)
|
||||||
|
: html`<span class="letter-fallback">${app.name.charAt(0).toUpperCase()}</span>`}
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="app-name">${app.name}</div>
|
||||||
|
<div class="category-badge">${app.category}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="app-description">${app.description}</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<span class="image-tag" title="${app.image}">${app.image}</span>
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="details-button" @click=${() => this.handleViewDetails(app)}>
|
||||||
|
View Details
|
||||||
|
</button>
|
||||||
|
<button class="deploy-button" @click=${() => this.handleDeploy(app)}>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="20 12 20 22 4 22 4 12"></polyline>
|
||||||
|
<rect x="2" y="7" width="20" height="5"></rect>
|
||||||
|
<line x1="12" y1="22" x2="12" y2="7"></line>
|
||||||
|
<path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"></path>
|
||||||
|
<path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"></path>
|
||||||
|
</svg>
|
||||||
|
Deploy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderIconByName(name: string): TemplateResult {
|
||||||
|
const icons: Record<string, TemplateResult> = {
|
||||||
|
'file-text': html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>`,
|
||||||
|
'git-branch': html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="6" y1="3" x2="6" y2="15"></line><circle cx="18" cy="6" r="3"></circle><circle cx="6" cy="18" r="3"></circle><path d="M18 9a9 9 0 0 1-9 9"></path></svg>`,
|
||||||
|
'book-open': html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg>`,
|
||||||
|
'globe': html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>`,
|
||||||
|
'database': html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path></svg>`,
|
||||||
|
'server': html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>`,
|
||||||
|
'package': html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="16.5" y1="9.4" x2="7.5" y2="4.21"></line><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>`,
|
||||||
|
'mail': html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>`,
|
||||||
|
'shield': html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>`,
|
||||||
|
'monitor': html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return icons[name] || html`<span class="letter-fallback">?</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleViewDetails(app: IAppTemplate) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('view-app', {
|
||||||
|
detail: { app },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDeploy(app: IAppTemplate) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('deploy-app', {
|
||||||
|
detail: { app },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,13 @@ export interface IConfigSectionAction {
|
|||||||
detail?: any;
|
detail?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IConfigSectionLink {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
icon?: string;
|
||||||
|
external?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'sz-config-section': SzConfigSection;
|
'sz-config-section': SzConfigSection;
|
||||||
@@ -46,6 +53,12 @@ export class SzConfigSection extends DeesElement {
|
|||||||
{ key: 'Auto Renew', value: true, type: 'boolean' },
|
{ key: 'Auto Renew', value: true, type: 'boolean' },
|
||||||
{ key: 'Renew Threshold', value: '30 days' },
|
{ key: 'Renew Threshold', value: '30 days' },
|
||||||
] as IConfigField[]}
|
] as IConfigField[]}
|
||||||
|
.links=${[
|
||||||
|
{ label: 'Docs', href: 'https://code.foss.global/serve.zone/smartproxy', icon: 'lucide:bookOpen', external: true },
|
||||||
|
] as IConfigSectionLink[]}
|
||||||
|
.actions=${[
|
||||||
|
{ label: 'Configure', icon: 'lucide:settings', event: 'configure' },
|
||||||
|
] as IConfigSectionAction[]}
|
||||||
></sz-config-section>
|
></sz-config-section>
|
||||||
<sz-config-section
|
<sz-config-section
|
||||||
title="Email Server"
|
title="Email Server"
|
||||||
@@ -57,6 +70,13 @@ export class SzConfigSection extends DeesElement {
|
|||||||
{ key: 'Hostname', value: null },
|
{ key: 'Hostname', value: null },
|
||||||
{ key: 'Domains', value: ['example.com', 'mail.example.com'], type: 'pills' },
|
{ key: 'Domains', value: ['example.com', 'mail.example.com'], type: 'pills' },
|
||||||
] as IConfigField[]}
|
] as IConfigField[]}
|
||||||
|
.links=${[
|
||||||
|
{ label: 'Docs', href: 'https://code.foss.global/serve.zone/smartmta', icon: 'lucide:bookOpen', external: true },
|
||||||
|
{ label: 'Source', href: 'https://code.foss.global/serve.zone/smartmta', icon: 'lucide:github', external: true },
|
||||||
|
] as IConfigSectionLink[]}
|
||||||
|
.actions=${[
|
||||||
|
{ label: 'Enable', icon: 'lucide:power', event: 'enable' },
|
||||||
|
] as IConfigSectionAction[]}
|
||||||
></sz-config-section>
|
></sz-config-section>
|
||||||
<sz-config-section
|
<sz-config-section
|
||||||
title="DNS Server"
|
title="DNS Server"
|
||||||
@@ -68,6 +88,9 @@ export class SzConfigSection extends DeesElement {
|
|||||||
{ key: 'Port', value: 53 },
|
{ key: 'Port', value: 53 },
|
||||||
{ key: 'NS Domains', value: ['ns1.example.com', 'ns2.example.com'], type: 'pills' },
|
{ key: 'NS Domains', value: ['ns1.example.com', 'ns2.example.com'], type: 'pills' },
|
||||||
] as IConfigField[]}
|
] as IConfigField[]}
|
||||||
|
.links=${[
|
||||||
|
{ label: 'Getting Started', href: 'https://docs.example.com/dns', icon: 'lucide:bookOpen', external: true },
|
||||||
|
] as IConfigSectionLink[]}
|
||||||
></sz-config-section>
|
></sz-config-section>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -91,6 +114,9 @@ export class SzConfigSection extends DeesElement {
|
|||||||
@property({ type: Array })
|
@property({ type: Array })
|
||||||
public accessor actions: IConfigSectionAction[] = [];
|
public accessor actions: IConfigSectionAction[] = [];
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor links: IConfigSectionLink[] = [];
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
public accessor collapsible: boolean = false;
|
public accessor collapsible: boolean = false;
|
||||||
|
|
||||||
@@ -100,6 +126,9 @@ export class SzConfigSection extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
accessor isCollapsed: boolean = false;
|
accessor isCollapsed: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor hasSlottedContent: boolean = false;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
css`
|
css`
|
||||||
@@ -108,20 +137,25 @@ export class SzConfigSection extends DeesElement {
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
dees-tile {
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
display: block;
|
||||||
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
}
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
:host([collapsed]) dees-tile::part(content) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([collapsed]) dees-tile::part(footer) {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
padding: 10px 16px;
|
||||||
padding: 14px 20px;
|
gap: 12px;
|
||||||
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
width: 100%;
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
box-sizing: border-box;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
@@ -131,30 +165,31 @@ export class SzConfigSection extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:host([collapsible]) .section-header:hover {
|
:host([collapsible]) .section-header:hover {
|
||||||
background: ${cssManager.bdTheme('#ebebed', '#1c1c1f')};
|
background: var(--dees-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-left {
|
.header-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-icon {
|
.header-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 36px;
|
width: 28px;
|
||||||
height: 36px;
|
height: 28px;
|
||||||
background: ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
background: var(--dees-color-border-default);
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-icon dees-icon {
|
.header-icon dees-icon {
|
||||||
font-size: 18px;
|
font-size: 14px;
|
||||||
color: ${cssManager.bdTheme('#52525b', '#a1a1aa')};
|
color: var(--dees-color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-text {
|
.header-text {
|
||||||
@@ -162,15 +197,17 @@ export class SzConfigSection extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-title {
|
.header-title {
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--dees-color-text-secondary);
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-subtitle {
|
.header-subtitle {
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
color: var(--dees-color-text-muted);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
}
|
}
|
||||||
@@ -236,30 +273,60 @@ export class SzConfigSection extends DeesElement {
|
|||||||
background: ${cssManager.bdTheme('#f59e0b', '#fbbf24')};
|
background: ${cssManager.bdTheme('#f59e0b', '#fbbf24')};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Action buttons */
|
/* Footer action buttons — canonical dees-modal / dees-tile pattern */
|
||||||
.header-action {
|
.section-footer {
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 0;
|
||||||
padding: 4px 12px;
|
height: 36px;
|
||||||
border-radius: 6px;
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button {
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
border-left: 1px solid var(--dees-color-border-subtle);
|
||||||
transition: background 150ms ease;
|
color: var(--dees-color-text-muted);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-action:hover {
|
.tile-button:first-child {
|
||||||
background: ${cssManager.bdTheme('rgba(37,99,235,0.08)', 'rgba(96,165,250,0.1)')};
|
border-left: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-action dees-icon {
|
.tile-button:hover {
|
||||||
font-size: 14px;
|
background: var(--dees-color-hover);
|
||||||
|
color: var(--dees-color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.primary {
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.primary:hover {
|
||||||
|
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button dees-icon {
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chevron */
|
/* Chevron */
|
||||||
@@ -274,8 +341,8 @@ export class SzConfigSection extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chevron dees-icon {
|
.chevron dees-icon {
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
|
color: var(--dees-color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Content */
|
/* Content */
|
||||||
@@ -283,8 +350,9 @@ export class SzConfigSection extends DeesElement {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-content.collapsed {
|
.fields-list {
|
||||||
display: none;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Field rows */
|
/* Field rows */
|
||||||
@@ -297,7 +365,7 @@ export class SzConfigSection extends DeesElement {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-row:last-child {
|
.fields-list .field-row:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,9 +480,8 @@ export class SzConfigSection extends DeesElement {
|
|||||||
border-top: 1px solid ${cssManager.bdTheme('#f4f4f5', '#1a1a1e')};
|
border-top: 1px solid ${cssManager.bdTheme('#f4f4f5', '#1a1a1e')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.slot-content:empty {
|
.slot-content.empty {
|
||||||
display: none;
|
display: none;
|
||||||
border-top: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Badge type */
|
/* Badge type */
|
||||||
@@ -439,6 +506,18 @@ export class SzConfigSection extends DeesElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updated(changedProperties: Map<string, any>) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
if (changedProperties.has('isCollapsed')) {
|
||||||
|
this.toggleAttribute('collapsed', this.isCollapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onSlotChange(e: Event) {
|
||||||
|
const slot = e.target as HTMLSlotElement;
|
||||||
|
this.hasSlottedContent = slot.assignedNodes({ flatten: true }).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
const statusLabels: Record<string, string> = {
|
const statusLabels: Record<string, string> = {
|
||||||
'enabled': 'Enabled',
|
'enabled': 'Enabled',
|
||||||
@@ -448,8 +527,9 @@ export class SzConfigSection extends DeesElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="section">
|
<dees-tile>
|
||||||
<div
|
<div
|
||||||
|
slot="header"
|
||||||
class="section-header"
|
class="section-header"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
if (this.collapsible) {
|
if (this.collapsible) {
|
||||||
@@ -475,8 +555,40 @@ export class SzConfigSection extends DeesElement {
|
|||||||
${statusLabels[this.status] || this.status}
|
${statusLabels[this.status] || this.status}
|
||||||
</span>
|
</span>
|
||||||
` : ''}
|
` : ''}
|
||||||
|
${this.collapsible ? html`
|
||||||
|
<span class="chevron ${this.isCollapsed ? 'collapsed' : ''}">
|
||||||
|
<dees-icon .icon=${'lucide:chevronDown'}></dees-icon>
|
||||||
|
</span>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
${this.fields.length > 0 ? html`
|
||||||
|
<div class="fields-list">
|
||||||
|
${this.fields.map(field => this.renderField(field))}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<div class="slot-content ${!this.hasSlottedContent ? 'empty' : ''}">
|
||||||
|
<slot @slotchange=${this.onSlotChange}></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${this.links.length > 0 || this.actions.length > 0 ? html`
|
||||||
|
<div slot="footer" class="section-footer">
|
||||||
|
${this.links.map(link => html`
|
||||||
|
<a
|
||||||
|
class="tile-button"
|
||||||
|
href=${link.href}
|
||||||
|
target=${link.external ? '_blank' : '_self'}
|
||||||
|
rel=${link.external ? 'noopener noreferrer' : ''}
|
||||||
|
@click=${(e: Event) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
${link.icon ? html`<dees-icon .icon=${link.icon}></dees-icon>` : ''}
|
||||||
|
${link.label}
|
||||||
|
${link.external ? html`<dees-icon .icon=${'lucide:externalLink'}></dees-icon>` : ''}
|
||||||
|
</a>
|
||||||
|
`)}
|
||||||
${this.actions.map(action => html`
|
${this.actions.map(action => html`
|
||||||
<button class="header-action" @click=${(e: Event) => {
|
<button class="tile-button primary" @click=${(e: Event) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.dispatchEvent(new CustomEvent(action.event || 'action', {
|
this.dispatchEvent(new CustomEvent(action.event || 'action', {
|
||||||
detail: action.detail || { label: action.label },
|
detail: action.detail || { label: action.label },
|
||||||
@@ -488,20 +600,9 @@ export class SzConfigSection extends DeesElement {
|
|||||||
${action.label}
|
${action.label}
|
||||||
</button>
|
</button>
|
||||||
`)}
|
`)}
|
||||||
${this.collapsible ? html`
|
|
||||||
<span class="chevron ${this.isCollapsed ? 'collapsed' : ''}">
|
|
||||||
<dees-icon .icon=${'lucide:chevronDown'}></dees-icon>
|
|
||||||
</span>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
` : ''}
|
||||||
<div class="section-content ${this.isCollapsed ? 'collapsed' : ''}">
|
</dees-tile>
|
||||||
${this.fields.map(field => this.renderField(field))}
|
|
||||||
<div class="slot-content">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export class SzDashboardView extends DeesElement {
|
|||||||
platformServices: [
|
platformServices: [
|
||||||
{ name: 'MongoDB', status: '1 DB', running: true },
|
{ name: 'MongoDB', status: '1 DB', running: true },
|
||||||
{ name: 'S3 Storage (MinIO)', status: '1 bucket', running: true },
|
{ name: 'S3 Storage (MinIO)', status: '1 bucket', running: true },
|
||||||
{ name: 'Caddy Reverse Proxy', status: 'Running', running: true },
|
{ name: 'SmartProxy Reverse Proxy', status: 'Running', running: true },
|
||||||
{ name: 'ClickHouse', status: '1 DB', running: true },
|
{ name: 'ClickHouse', status: '1 DB', running: true },
|
||||||
],
|
],
|
||||||
traffic: {
|
traffic: {
|
||||||
|
|||||||
@@ -229,57 +229,87 @@ export class SzDomainDetailView extends DeesElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
dees-tile.full-width {
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section.full-width {
|
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
|
height: 36px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 14px 16px;
|
padding: 0 16px;
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
width: 100%;
|
||||||
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 14px;
|
flex: 1;
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--dees-color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title svg {
|
.section-title svg {
|
||||||
width: 16px;
|
width: 14px;
|
||||||
height: 16px;
|
height: 14px;
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
flex-shrink: 0;
|
||||||
|
color: var(--dees-color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-action {
|
.section-footer {
|
||||||
padding: 6px 10px;
|
display: flex;
|
||||||
background: transparent;
|
flex-direction: row;
|
||||||
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
justify-content: flex-end;
|
||||||
border-radius: 4px;
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
height: 36px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button {
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 200ms ease;
|
user-select: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid var(--dees-color-border-subtle);
|
||||||
|
color: var(--dees-color-text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-action:hover {
|
.tile-button:first-child {
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
border-left: none;
|
||||||
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tile-button:hover {
|
||||||
|
background: var(--dees-color-hover);
|
||||||
|
color: var(--dees-color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.primary {
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.primary:hover {
|
||||||
|
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.section-content {
|
.section-content {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
@@ -582,8 +612,8 @@ export class SzDomainDetailView extends DeesElement {
|
|||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<!-- Certificate Section -->
|
<!-- Certificate Section -->
|
||||||
<div class="section">
|
<dees-tile>
|
||||||
<div class="section-header">
|
<div slot="header" class="section-header">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||||
@@ -591,9 +621,6 @@ export class SzDomainDetailView extends DeesElement {
|
|||||||
</svg>
|
</svg>
|
||||||
SSL Certificate
|
SSL Certificate
|
||||||
</div>
|
</div>
|
||||||
${this.certificate ? html`
|
|
||||||
<button class="section-action" @click=${() => this.handleRenewCertificate()}>Renew</button>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="section-content">
|
<div class="section-content">
|
||||||
${this.certificate ? html`
|
${this.certificate ? html`
|
||||||
@@ -652,11 +679,16 @@ export class SzDomainDetailView extends DeesElement {
|
|||||||
<div class="empty-state">No certificate configured</div>
|
<div class="empty-state">No certificate configured</div>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
${this.certificate ? html`
|
||||||
|
<div slot="footer" class="section-footer">
|
||||||
|
<button class="tile-button primary" @click=${() => this.handleRenewCertificate()}>Renew</button>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</dees-tile>
|
||||||
|
|
||||||
<!-- Proxy Routes Section -->
|
<!-- Proxy Routes Section -->
|
||||||
<div class="section">
|
<dees-tile>
|
||||||
<div class="section-header">
|
<div slot="header" class="section-header">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<polyline points="16 3 21 3 21 8"></polyline>
|
<polyline points="16 3 21 3 21 8"></polyline>
|
||||||
@@ -679,11 +711,11 @@ export class SzDomainDetailView extends DeesElement {
|
|||||||
<div class="empty-state">No proxy routes configured</div>
|
<div class="empty-state">No proxy routes configured</div>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dees-tile>
|
||||||
|
|
||||||
<!-- DNS Records Section -->
|
<!-- DNS Records Section -->
|
||||||
<div class="section full-width">
|
<dees-tile class="full-width">
|
||||||
<div class="section-header">
|
<div slot="header" class="section-header">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
@@ -692,13 +724,6 @@ export class SzDomainDetailView extends DeesElement {
|
|||||||
</svg>
|
</svg>
|
||||||
DNS Records
|
DNS Records
|
||||||
</div>
|
</div>
|
||||||
<button class="section-action" @click=${() => this.handleAddDnsRecord()}>
|
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px;">
|
|
||||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
||||||
</svg>
|
|
||||||
Add Record
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="section-content">
|
<div class="section-content">
|
||||||
${this.dnsRecords.length > 0 ? html`
|
${this.dnsRecords.length > 0 ? html`
|
||||||
@@ -737,7 +762,16 @@ export class SzDomainDetailView extends DeesElement {
|
|||||||
<div class="empty-state">No DNS records configured</div>
|
<div class="empty-state">No DNS records configured</div>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div slot="footer" class="section-footer">
|
||||||
|
<button class="tile-button primary" @click=${() => this.handleAddDnsRecord()}>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
Add Record
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</dees-tile>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,31 +256,89 @@ export class SzMtaDetailView extends DeesElement {
|
|||||||
gap: 24px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card-header {
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
height: 36px;
|
||||||
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
display: flex;
|
||||||
border-radius: 8px;
|
align-items: center;
|
||||||
overflow: hidden;
|
padding: 0 16px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-heading {
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
align-items: baseline;
|
||||||
align-items: center;
|
gap: 8px;
|
||||||
padding: 16px;
|
min-width: 0;
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
font-size: 16px;
|
font-weight: 500;
|
||||||
font-weight: 600;
|
font-size: 13px;
|
||||||
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--dees-color-text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-subtitle {
|
.card-subtitle {
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
color: var(--dees-color-text-muted);
|
||||||
margin-top: 2px;
|
letter-spacing: -0.01em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
height: 36px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button {
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid var(--dees-color-border-subtle);
|
||||||
|
color: var(--dees-color-text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button:first-child {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button:hover {
|
||||||
|
background: var(--dees-color-hover);
|
||||||
|
color: var(--dees-color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.primary {
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.primary:hover {
|
||||||
|
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-content {
|
.card-content {
|
||||||
@@ -451,37 +509,13 @@ export class SzMtaDetailView extends DeesElement {
|
|||||||
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Copy button */
|
/* SMTP metadata banner — sits inside content, above the log */
|
||||||
.smtp-copy-button {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
|
||||||
background: transparent;
|
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 150ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.smtp-copy-button:hover {
|
|
||||||
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
|
||||||
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header subtitle enhancements */
|
|
||||||
.smtp-header-subtitle {
|
.smtp-header-subtitle {
|
||||||
font-size: 13px;
|
padding: 10px 16px;
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
font-size: 12px;
|
||||||
margin-top: 2px;
|
color: var(--dees-color-text-muted);
|
||||||
display: flex;
|
border-bottom: 1px solid var(--dees-color-border-subtle);
|
||||||
align-items: center;
|
font-family: monospace;
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.smtp-direction-badge {
|
.smtp-direction-badge {
|
||||||
@@ -578,7 +612,7 @@ export class SzMtaDetailView extends DeesElement {
|
|||||||
color: ${cssManager.bdTheme('#ca8a04', '#facc15')};
|
color: ${cssManager.bdTheme('#ca8a04', '#facc15')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.rejection-card {
|
dees-tile.rejection-card::part(outer) {
|
||||||
border-color: ${cssManager.bdTheme('#fecaca', 'rgba(239, 68, 68, 0.3)')};
|
border-color: ${cssManager.bdTheme('#fecaca', 'rgba(239, 68, 68, 0.3)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,9 +680,11 @@ export class SzMtaDetailView extends DeesElement {
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<!-- Email Metadata -->
|
<!-- Email Metadata -->
|
||||||
<div class="card">
|
<dees-tile>
|
||||||
<div class="card-header">
|
<div slot="header" class="card-header">
|
||||||
<div class="card-title">Email Metadata</div>
|
<div class="card-heading">
|
||||||
|
<span class="card-title">Email Metadata</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="detail-list">
|
<div class="detail-list">
|
||||||
@@ -684,49 +720,53 @@ export class SzMtaDetailView extends DeesElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dees-tile>
|
||||||
|
|
||||||
<!-- SMTP Transaction Log -->
|
<!-- SMTP Transaction Log -->
|
||||||
<div class="card">
|
<dees-tile>
|
||||||
<div class="card-header">
|
<div slot="header" class="card-header">
|
||||||
<div>
|
<div class="card-heading">
|
||||||
<div class="card-title">SMTP Transaction Log</div>
|
<span class="card-title">SMTP Transaction Log</span>
|
||||||
<div class="smtp-header-subtitle">
|
<span class="smtp-direction-badge ${email.direction}">${email.direction}</span>
|
||||||
<span class="smtp-direction-badge ${email.direction}">${email.direction}</span>
|
|
||||||
<span>${email.direction === 'outbound'
|
|
||||||
? `${email.connectionInfo.sourceHostname} → ${email.connectionInfo.destinationIp}:${email.connectionInfo.destinationPort}`
|
|
||||||
: `${email.connectionInfo.sourceIp} → ${email.connectionInfo.sourceHostname}:${email.connectionInfo.destinationPort}`
|
|
||||||
}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="smtp-copy-button" @click=${() => this.copySmtpLog()}>
|
</div>
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<div class="smtp-header-subtitle">
|
||||||
|
${email.direction === 'outbound'
|
||||||
|
? `${email.connectionInfo.sourceHostname} → ${email.connectionInfo.destinationIp}:${email.connectionInfo.destinationPort}`
|
||||||
|
: `${email.connectionInfo.sourceIp} → ${email.connectionInfo.sourceHostname}:${email.connectionInfo.destinationPort}`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
${this.renderSmtpLog(email)}
|
||||||
|
<div slot="footer" class="card-footer">
|
||||||
|
<button class="tile-button" @click=${() => this.copySmtpLog()}>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||||
</svg>
|
</svg>
|
||||||
Copy Log
|
Copy Log
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
${this.renderSmtpLog(email)}
|
</dees-tile>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Email Body -->
|
<!-- Email Body -->
|
||||||
<div class="card">
|
<dees-tile>
|
||||||
<div class="card-header">
|
<div slot="header" class="card-header">
|
||||||
<div>
|
<div class="card-heading">
|
||||||
<div class="card-title">Email Body (Escaped)</div>
|
<span class="card-title">Email Body (Escaped)</span>
|
||||||
<div class="card-subtitle">Raw content — HTML is not rendered</div>
|
<span class="card-subtitle">Raw content — HTML is not rendered</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<pre class="email-body-container">${email.body}</pre>
|
<pre class="email-body-container">${email.body}</pre>
|
||||||
</div>
|
</dees-tile>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<!-- Connection Info -->
|
<!-- Connection Info -->
|
||||||
<div class="card">
|
<dees-tile>
|
||||||
<div class="card-header">
|
<div slot="header" class="card-header">
|
||||||
<div class="card-title">Connection Info</div>
|
<div class="card-heading">
|
||||||
|
<span class="card-title">Connection Info</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="detail-list">
|
<div class="detail-list">
|
||||||
@@ -772,12 +812,14 @@ export class SzMtaDetailView extends DeesElement {
|
|||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dees-tile>
|
||||||
|
|
||||||
<!-- Authentication Results -->
|
<!-- Authentication Results -->
|
||||||
<div class="card">
|
<dees-tile>
|
||||||
<div class="card-header">
|
<div slot="header" class="card-header">
|
||||||
<div class="card-title">Authentication Results</div>
|
<div class="card-heading">
|
||||||
|
<span class="card-title">Authentication Results</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="auth-row">
|
<div class="auth-row">
|
||||||
@@ -802,13 +844,15 @@ export class SzMtaDetailView extends DeesElement {
|
|||||||
<span class="auth-badge ${email.authenticationResults.dmarc}">${email.authenticationResults.dmarc}</span>
|
<span class="auth-badge ${email.authenticationResults.dmarc}">${email.authenticationResults.dmarc}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dees-tile>
|
||||||
|
|
||||||
<!-- Rejection Details (conditional) -->
|
<!-- Rejection Details (conditional) -->
|
||||||
${email.status === 'rejected' || email.status === 'bounced' ? html`
|
${email.status === 'rejected' || email.status === 'bounced' ? html`
|
||||||
<div class="card rejection-card">
|
<dees-tile class="rejection-card">
|
||||||
<div class="card-header">
|
<div slot="header" class="card-header">
|
||||||
<div class="card-title">Rejection Details</div>
|
<div class="card-heading">
|
||||||
|
<span class="card-title">Rejection Details</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
${email.rejectionReason ? html`
|
${email.rejectionReason ? html`
|
||||||
@@ -820,7 +864,7 @@ export class SzMtaDetailView extends DeesElement {
|
|||||||
<div class="rejection-text">${email.bounceMessage}</div>
|
<div class="rejection-text">${email.bounceMessage}</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dees-tile>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import {
|
|||||||
property,
|
property,
|
||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
import type { IStatsTile } from '@design.estate/dees-catalog';
|
||||||
import './sz-stat-card.js';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -91,19 +90,11 @@ export class SzNetworkDomainsView extends DeesElement {
|
|||||||
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
|
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-grid {
|
dees-statsgrid {
|
||||||
display: grid;
|
display: block;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-container {
|
.table-container {
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||||
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
||||||
@@ -205,6 +196,42 @@ export class SzNetworkDomainsView extends DeesElement {
|
|||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private get tiles(): IStatsTile[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'total',
|
||||||
|
title: 'Total Domains',
|
||||||
|
value: this.stats.total,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:globe',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'valid',
|
||||||
|
title: 'Valid Certificates',
|
||||||
|
value: this.stats.valid,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:shieldCheck',
|
||||||
|
color: '#22c55e',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'expiring',
|
||||||
|
title: 'Expiring Soon',
|
||||||
|
value: this.stats.expiring,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:shieldAlert',
|
||||||
|
color: this.stats.expiring > 0 ? '#f59e0b' : undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'expired',
|
||||||
|
title: 'Expired/Pending',
|
||||||
|
value: this.stats.expired,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:circleOff',
|
||||||
|
color: this.stats.expired > 0 ? '#ef4444' : undefined,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="header">
|
<div class="header">
|
||||||
@@ -212,31 +239,10 @@ export class SzNetworkDomainsView extends DeesElement {
|
|||||||
<button class="sync-button" @click=${() => this.handleSync()}>Sync Cloudflare</button>
|
<button class="sync-button" @click=${() => this.handleSync()}>Sync Cloudflare</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stats-grid">
|
<dees-statsgrid
|
||||||
<sz-stat-card
|
.tiles=${this.tiles}
|
||||||
label="Total Domains"
|
.minTileWidth=${200}
|
||||||
value="${this.stats.total}"
|
></dees-statsgrid>
|
||||||
icon="server"
|
|
||||||
></sz-stat-card>
|
|
||||||
<sz-stat-card
|
|
||||||
label="Valid Certificates"
|
|
||||||
value="${this.stats.valid}"
|
|
||||||
icon="check"
|
|
||||||
variant="success"
|
|
||||||
></sz-stat-card>
|
|
||||||
<sz-stat-card
|
|
||||||
label="Expiring Soon"
|
|
||||||
value="${this.stats.expiring}"
|
|
||||||
icon="stop"
|
|
||||||
variant="${this.stats.expiring > 0 ? 'warning' : 'default'}"
|
|
||||||
></sz-stat-card>
|
|
||||||
<sz-stat-card
|
|
||||||
label="Expired/Pending"
|
|
||||||
value="${this.stats.expired}"
|
|
||||||
icon="stop"
|
|
||||||
variant="${this.stats.expired > 0 ? 'error' : 'default'}"
|
|
||||||
></sz-stat-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import {
|
|||||||
property,
|
property,
|
||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
import type { IStatsTile } from '@design.estate/dees-catalog';
|
||||||
import './sz-stat-card.js';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -113,42 +112,107 @@ export class SzNetworkProxyView extends DeesElement {
|
|||||||
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-grid {
|
dees-statsgrid {
|
||||||
display: grid;
|
display: block;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
dees-tile {
|
||||||
.stats-grid {
|
display: block;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
padding: 16px;
|
height: 36px;
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 16px;
|
font-weight: 500;
|
||||||
font-weight: 600;
|
font-size: 13px;
|
||||||
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--dees-color-text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-subtitle {
|
.section-subtitle {
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
color: var(--dees-color-text-muted);
|
||||||
margin-top: 2px;
|
letter-spacing: -0.01em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
height: 36px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button {
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid var(--dees-color-border-subtle);
|
||||||
|
color: var(--dees-color-text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button:first-child {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button:hover {
|
||||||
|
background: var(--dees-color-hover);
|
||||||
|
color: var(--dees-color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.primary {
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.primary:hover {
|
||||||
|
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.danger {
|
||||||
|
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.danger:hover {
|
||||||
|
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.1)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-header {
|
.table-header {
|
||||||
@@ -234,61 +298,6 @@ export class SzNetworkProxyView extends DeesElement {
|
|||||||
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px;
|
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stream-button {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
background: ${cssManager.bdTheme('#2563eb', '#3b82f6')};
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: white;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 200ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stream-button:hover {
|
|
||||||
background: ${cssManager.bdTheme('#1d4ed8', '#2563eb')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.stream-button.streaming {
|
|
||||||
background: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.stream-button.streaming:hover {
|
|
||||||
background: ${cssManager.bdTheme('#b91c1c', '#dc2626')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-button {
|
|
||||||
padding: 6px 12px;
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 200ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-button:hover {
|
|
||||||
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs-container {
|
.logs-container {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
@@ -336,41 +345,57 @@ export class SzNetworkProxyView extends DeesElement {
|
|||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private get tiles(): IStatsTile[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'proxy-status',
|
||||||
|
title: 'Proxy Status',
|
||||||
|
value: this.proxyStatus === 'running' ? 'Running' : 'Stopped',
|
||||||
|
type: 'text',
|
||||||
|
icon: 'lucide:server',
|
||||||
|
color: this.proxyStatus === 'running' ? '#22c55e' : '#ef4444',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'routes',
|
||||||
|
title: 'Routes',
|
||||||
|
value: this.routeCount,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:server',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'certificates',
|
||||||
|
title: 'Certificates',
|
||||||
|
value: this.certificateCount,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:check',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'targets',
|
||||||
|
title: 'Targets',
|
||||||
|
value: this.targetCount,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:server',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="refresh-button" @click=${() => this.handleRefresh()}>Refresh</button>
|
<button class="refresh-button" @click=${() => this.handleRefresh()}>Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stats-grid">
|
<dees-statsgrid
|
||||||
<sz-stat-card
|
.tiles=${this.tiles}
|
||||||
label="Proxy Status"
|
.minTileWidth=${200}
|
||||||
value="${this.proxyStatus === 'running' ? 'Running' : 'Stopped'}"
|
></dees-statsgrid>
|
||||||
icon="server"
|
|
||||||
variant="${this.proxyStatus === 'running' ? 'success' : 'error'}"
|
|
||||||
valueBadge
|
|
||||||
></sz-stat-card>
|
|
||||||
<sz-stat-card
|
|
||||||
label="Routes"
|
|
||||||
value="${this.routeCount}"
|
|
||||||
icon="server"
|
|
||||||
></sz-stat-card>
|
|
||||||
<sz-stat-card
|
|
||||||
label="Certificates"
|
|
||||||
value="${this.certificateCount}"
|
|
||||||
icon="check"
|
|
||||||
></sz-stat-card>
|
|
||||||
<sz-stat-card
|
|
||||||
label="Targets"
|
|
||||||
value="${this.targetCount}"
|
|
||||||
icon="server"
|
|
||||||
></sz-stat-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
<dees-tile>
|
||||||
<div class="section-header">
|
<div slot="header" class="section-header">
|
||||||
<div class="section-title">Traffic Targets</div>
|
<div class="section-heading">
|
||||||
<div class="section-subtitle">Services, registry, and platform services with their routing info</div>
|
<span class="section-title">Traffic Targets</span>
|
||||||
|
<span class="section-subtitle">Services, registry, and platform services with their routing info</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<span>Type</span>
|
<span>Type</span>
|
||||||
@@ -388,30 +413,13 @@ export class SzNetworkProxyView extends DeesElement {
|
|||||||
<span><span class="status-badge ${target.status}">${target.status}</span></span>
|
<span><span class="status-badge ${target.status}">${target.status}</span></span>
|
||||||
</div>
|
</div>
|
||||||
`)}
|
`)}
|
||||||
</div>
|
</dees-tile>
|
||||||
|
|
||||||
<div class="section">
|
<dees-tile>
|
||||||
<div class="logs-header">
|
<div slot="header" class="section-header">
|
||||||
<div>
|
<div class="section-heading">
|
||||||
<div class="section-title">Access Logs</div>
|
<span class="section-title">Access Logs</span>
|
||||||
<div class="section-subtitle">Real-time Caddy access logs</div>
|
<span class="section-subtitle">Real-time proxy access logs</span>
|
||||||
</div>
|
|
||||||
<div class="logs-actions">
|
|
||||||
<button class="stream-button ${this.streaming ? 'streaming' : ''}" @click=${() => this.toggleStreaming()}>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
${this.streaming
|
|
||||||
? html`<rect x="6" y="6" width="12" height="12" rx="1"/>`
|
|
||||||
: html`<polygon points="5,3 19,12 5,21"/>`
|
|
||||||
}
|
|
||||||
</svg>
|
|
||||||
${this.streaming ? 'Stop' : 'Stream'}
|
|
||||||
</button>
|
|
||||||
<button class="clear-button" @click=${() => this.handleClearLogs()}>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<polyline points="3,6 5,6 21,6"/><path d="M19,6v14a2,2,0,0,1-2,2H7a2,2,0,0,1-2-2V6m3,0V4a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2v2"/>
|
|
||||||
</svg>
|
|
||||||
Clear logs
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="logs-container">
|
<div class="logs-container">
|
||||||
@@ -428,7 +436,24 @@ export class SzNetworkProxyView extends DeesElement {
|
|||||||
<div class="empty-logs">Click "Stream" to start live access log streaming</div>
|
<div class="empty-logs">Click "Stream" to start live access log streaming</div>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div slot="footer" class="section-footer">
|
||||||
|
<button class="tile-button ${this.streaming ? 'danger' : 'primary'}" @click=${() => this.toggleStreaming()}>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
${this.streaming
|
||||||
|
? html`<rect x="6" y="6" width="12" height="12" rx="1"/>`
|
||||||
|
: html`<polygon points="5,3 19,12 5,21"/>`
|
||||||
|
}
|
||||||
|
</svg>
|
||||||
|
${this.streaming ? 'Stop' : 'Stream'}
|
||||||
|
</button>
|
||||||
|
<button class="tile-button" @click=${() => this.handleClearLogs()}>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="3,6 5,6 21,6"/><path d="M19,6v14a2,2,0,0,1-2,2H7a2,2,0,0,1-2-2V6m3,0V4a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2v2"/>
|
||||||
|
</svg>
|
||||||
|
Clear logs
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</dees-tile>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -225,39 +225,35 @@ export class SzPlatformServiceDetailView extends DeesElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
dees-tile.full-width {
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section.full-width {
|
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
|
height: 36px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 14px 16px;
|
padding: 0 16px;
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
width: 100%;
|
||||||
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 14px;
|
flex: 1;
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--dees-color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title svg {
|
.section-title svg {
|
||||||
width: 16px;
|
width: 14px;
|
||||||
height: 16px;
|
height: 14px;
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
flex-shrink: 0;
|
||||||
|
color: var(--dees-color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-content {
|
.section-content {
|
||||||
@@ -452,8 +448,8 @@ export class SzPlatformServiceDetailView extends DeesElement {
|
|||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<!-- Connection Info -->
|
<!-- Connection Info -->
|
||||||
<div class="section">
|
<dees-tile>
|
||||||
<div class="section-header">
|
<div slot="header" class="section-header">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
|
||||||
@@ -516,11 +512,11 @@ export class SzPlatformServiceDetailView extends DeesElement {
|
|||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dees-tile>
|
||||||
|
|
||||||
<!-- Configuration -->
|
<!-- Configuration -->
|
||||||
<div class="section">
|
<dees-tile>
|
||||||
<div class="section-header">
|
<div slot="header" class="section-header">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<circle cx="12" cy="12" r="3"></circle>
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
@@ -537,12 +533,12 @@ export class SzPlatformServiceDetailView extends DeesElement {
|
|||||||
</div>
|
</div>
|
||||||
`)}
|
`)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dees-tile>
|
||||||
|
|
||||||
<!-- Metrics -->
|
<!-- Metrics -->
|
||||||
${this.service.metrics ? html`
|
${this.service.metrics ? html`
|
||||||
<div class="section full-width">
|
<dees-tile class="full-width">
|
||||||
<div class="section-header">
|
<div slot="header" class="section-header">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<line x1="18" y1="20" x2="18" y2="10"></line>
|
<line x1="18" y1="20" x2="18" y2="10"></line>
|
||||||
@@ -583,21 +579,23 @@ export class SzPlatformServiceDetailView extends DeesElement {
|
|||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dees-tile>
|
||||||
` : ''}
|
` : ''}
|
||||||
|
|
||||||
<!-- Logs -->
|
<!-- Logs -->
|
||||||
<dees-chart-log
|
<div style="grid-column: 1 / -1;">
|
||||||
.label=${'Service Logs'}
|
<dees-chart-log
|
||||||
.logEntries=${this.logs.map(log => ({
|
.label=${'Service Logs'}
|
||||||
timestamp: log.timestamp.includes('T') ? log.timestamp : new Date(log.timestamp).toISOString(),
|
.logEntries=${this.logs.map(log => ({
|
||||||
level: log.level as 'debug' | 'info' | 'warn' | 'error',
|
timestamp: log.timestamp.includes('T') ? log.timestamp : new Date(log.timestamp).toISOString(),
|
||||||
message: log.message,
|
level: log.level as 'debug' | 'info' | 'warn' | 'error',
|
||||||
}))}
|
message: log.message,
|
||||||
.autoScroll=${true}
|
}))}
|
||||||
.maxEntries=${2000}
|
.autoScroll=${true}
|
||||||
.showMetrics=${true}
|
.maxEntries=${2000}
|
||||||
></dees-chart-log>
|
.showMetrics=${true}
|
||||||
|
></dees-chart-log>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export class SzPlatformServicesCard extends DeesElement {
|
|||||||
.services=${[
|
.services=${[
|
||||||
{ name: 'MongoDB', status: '1 DB', running: true },
|
{ name: 'MongoDB', status: '1 DB', running: true },
|
||||||
{ name: 'S3 Storage (MinIO)', status: '1 bucket', running: true },
|
{ name: 'S3 Storage (MinIO)', status: '1 bucket', running: true },
|
||||||
{ name: 'Caddy Reverse Proxy', status: 'Running', running: true },
|
{ name: 'SmartProxy Reverse Proxy', status: 'Running', running: true },
|
||||||
{ name: 'ClickHouse', status: '1 DB', running: true },
|
{ name: 'ClickHouse', status: '1 DB', running: true },
|
||||||
]}
|
]}
|
||||||
></sz-platform-services-card>
|
></sz-platform-services-card>
|
||||||
|
|||||||
@@ -55,12 +55,23 @@ export interface IRouteSecurity {
|
|||||||
rateLimit?: { enabled: boolean; maxRequests: number; window: number };
|
rateLimit?: { enabled: boolean; maxRequests: number; window: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IRouteMetadata {
|
||||||
|
sourceProfileRef?: string;
|
||||||
|
networkTargetRef?: string;
|
||||||
|
sourceProfileName?: string;
|
||||||
|
networkTargetName?: string;
|
||||||
|
lastResolvedAt?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IRouteConfig {
|
export interface IRouteConfig {
|
||||||
id?: string;
|
id?: string;
|
||||||
match: IRouteMatch;
|
match: IRouteMatch;
|
||||||
action: IRouteAction;
|
action: IRouteAction;
|
||||||
security?: IRouteSecurity;
|
security?: IRouteSecurity;
|
||||||
headers?: { request?: Record<string, string>; response?: Record<string, string> };
|
headers?: { request?: Record<string, string>; response?: Record<string, string> };
|
||||||
|
metadata?: IRouteMetadata;
|
||||||
|
/** When true, only VPN clients whose TargetProfile matches this route get access */
|
||||||
|
vpnOnly?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
priority?: number;
|
priority?: number;
|
||||||
@@ -127,6 +138,11 @@ export class SzRouteCard extends DeesElement {
|
|||||||
rateLimit: { enabled: true, maxRequests: 100, window: 60 },
|
rateLimit: { enabled: true, maxRequests: 100, window: 60 },
|
||||||
maxConnections: 1000,
|
maxConnections: 1000,
|
||||||
},
|
},
|
||||||
|
vpnOnly: true,
|
||||||
|
metadata: {
|
||||||
|
sourceProfileName: 'STANDARD',
|
||||||
|
networkTargetName: 'LOSSLESS_INFRA',
|
||||||
|
},
|
||||||
} satisfies IRouteConfig}
|
} satisfies IRouteConfig}
|
||||||
></sz-route-card>
|
></sz-route-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,6 +153,9 @@ export class SzRouteCard extends DeesElement {
|
|||||||
@property({ type: Object })
|
@property({ type: Object })
|
||||||
public accessor route: IRouteConfig | null = null;
|
public accessor route: IRouteConfig | null = null;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public accessor showActions: boolean = false;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
css`
|
css`
|
||||||
@@ -144,26 +163,25 @@ export class SzRouteCard extends DeesElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
dees-tile::part(outer) {
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
transition: border-color 200ms ease, box-shadow 200ms ease;
|
transition: border-color 200ms ease, box-shadow 200ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:hover {
|
dees-tile:hover::part(outer) {
|
||||||
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
|
border-color: var(--dees-color-border-strong);
|
||||||
box-shadow: 0 2px 8px ${cssManager.bdTheme('rgba(0,0,0,0.06)', 'rgba(0,0,0,0.2)')};
|
box-shadow: 0 2px 8px ${cssManager.bdTheme('rgba(0,0,0,0.06)', 'rgba(0,0,0,0.2)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header */
|
/* Header */
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-bottom: 4px;
|
height: 40px;
|
||||||
|
padding: 0 16px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-left {
|
.header-left {
|
||||||
@@ -171,6 +189,7 @@ export class SzRouteCard extends DeesElement {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot {
|
.status-dot {
|
||||||
@@ -190,9 +209,10 @@ export class SzRouteCard extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.route-name {
|
.route-name {
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--dees-color-text-secondary);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -234,6 +254,10 @@ export class SzRouteCard extends DeesElement {
|
|||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||||
@@ -277,7 +301,7 @@ export class SzRouteCard extends DeesElement {
|
|||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
border-radius: 0 6px 6px 0;
|
border-radius: 0 6px 6px 0;
|
||||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
background: ${cssManager.bdTheme('#fafafa', '#101010')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.section:last-of-type {
|
.section:last-of-type {
|
||||||
@@ -296,6 +320,21 @@ export class SzRouteCard extends DeesElement {
|
|||||||
border-left-color: ${cssManager.bdTheme('#f59e0b', '#f59e0b')};
|
border-left-color: ${cssManager.bdTheme('#f59e0b', '#f59e0b')};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section.linked {
|
||||||
|
border-left-color: ${cssManager.bdTheme('#8b5cf6', '#8b5cf6')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.linked-name {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: monospace;
|
||||||
|
background: ${cssManager.bdTheme('#ede9fe', 'rgba(139, 92, 246, 0.15)')};
|
||||||
|
color: ${cssManager.bdTheme('#6d28d9', '#a78bfa')};
|
||||||
|
}
|
||||||
|
|
||||||
.section-label {
|
.section-label {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -431,12 +470,102 @@ export class SzRouteCard extends DeesElement {
|
|||||||
color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
|
color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section.vpn {
|
||||||
|
border-left-color: ${cssManager.bdTheme('#0891b2', '#06b6d4')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.vpn-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vpn-badge.mandatory {
|
||||||
|
background: ${cssManager.bdTheme('#fff7ed', 'rgba(249, 115, 22, 0.2)')};
|
||||||
|
color: ${cssManager.bdTheme('#c2410c', '#fb923c')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.vpn-badge.optional {
|
||||||
|
background: ${cssManager.bdTheme('#ecfdf5', 'rgba(16, 185, 129, 0.2)')};
|
||||||
|
color: ${cssManager.bdTheme('#047857', '#34d399')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.vpn-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: monospace;
|
||||||
|
margin-right: 4px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
background: ${cssManager.bdTheme('#ecfeff', 'rgba(6, 182, 212, 0.15)')};
|
||||||
|
color: ${cssManager.bdTheme('#0e7490', '#22d3ee')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
height: 36px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button {
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid var(--dees-color-border-subtle);
|
||||||
|
color: var(--dees-color-text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button:first-child {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button:hover {
|
||||||
|
background: var(--dees-color-hover);
|
||||||
|
color: var(--dees-color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.edit:hover {
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.delete:hover {
|
||||||
|
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.1)')};
|
||||||
|
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
if (!this.route) {
|
if (!this.route) {
|
||||||
return html`<div class="card"><div class="no-route">No route data</div></div>`;
|
return html`
|
||||||
|
<dees-tile>
|
||||||
|
<div class="card-body"><div class="no-route">No route data</div></div>
|
||||||
|
</dees-tile>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const r = this.route;
|
const r = this.route;
|
||||||
@@ -446,9 +575,9 @@ export class SzRouteCard extends DeesElement {
|
|||||||
const security = r.security;
|
const security = r.security;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="card">
|
<dees-tile>
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="header">
|
<div slot="header" class="header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<span class="status-dot ${isEnabled ? 'enabled' : 'disabled'}"></span>
|
<span class="status-dot ${isEnabled ? 'enabled' : 'disabled'}"></span>
|
||||||
<span class="route-name">${r.name || r.id || 'Unnamed Route'}</span>
|
<span class="route-name">${r.name || r.id || 'Unnamed Route'}</span>
|
||||||
@@ -459,16 +588,17 @@ export class SzRouteCard extends DeesElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${r.description ? html`<div class="description">${r.description}</div>` : ''}
|
<div class="card-body">
|
||||||
|
${r.description ? html`<div class="description">${r.description}</div>` : ''}
|
||||||
|
|
||||||
<div class="meta-row">
|
<div class="meta-row">
|
||||||
${r.tags && r.tags.length > 0
|
${r.tags && r.tags.length > 0
|
||||||
? html`<div class="tags">${r.tags.map((t) => html`<span class="tag">${t}</span>`)}</div>`
|
? html`<div class="tags">${r.tags.map((t) => html`<span class="tag">${t}</span>`)}</div>`
|
||||||
: html`<div></div>`}
|
: html`<div></div>`}
|
||||||
${r.priority != null ? html`<span class="priority">Priority: ${r.priority}</span>` : ''}
|
${r.priority != null ? html`<span class="priority">Priority: ${r.priority}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Match Section -->
|
<!-- Match Section -->
|
||||||
<div class="section match">
|
<div class="section match">
|
||||||
<div class="section-label">Match</div>
|
<div class="section-label">Match</div>
|
||||||
<div class="field-row">
|
<div class="field-row">
|
||||||
@@ -624,9 +754,38 @@ export class SzRouteCard extends DeesElement {
|
|||||||
`
|
`
|
||||||
: ''}
|
: ''}
|
||||||
|
|
||||||
<!-- Feature Icons Row -->
|
<!-- VPN Section -->
|
||||||
${this.renderFeatures()}
|
${this.renderVpn()}
|
||||||
</div>
|
|
||||||
|
<!-- Linked References Section -->
|
||||||
|
${this.renderLinked()}
|
||||||
|
|
||||||
|
<!-- Feature Icons Row -->
|
||||||
|
${this.renderFeatures()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
${this.showActions ? html`
|
||||||
|
<div slot="footer" class="card-footer">
|
||||||
|
<button class="tile-button edit" @click=${(e: Event) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.dispatchEvent(new CustomEvent('route-edit', {
|
||||||
|
detail: this.route,
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}));
|
||||||
|
}}>Edit</button>
|
||||||
|
<button class="tile-button delete" @click=${(e: Event) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.dispatchEvent(new CustomEvent('route-delete', {
|
||||||
|
detail: this.route,
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}));
|
||||||
|
}}>Delete</button>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</dees-tile>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,12 +797,59 @@ export class SzRouteCard extends DeesElement {
|
|||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderVpn(): TemplateResult {
|
||||||
|
if (!this.route?.vpnOnly) return html``;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="section vpn">
|
||||||
|
<div class="section-label">VPN Access</div>
|
||||||
|
<div class="field-row">
|
||||||
|
<span class="field-key">Mode</span>
|
||||||
|
<span class="field-value">
|
||||||
|
<span class="vpn-badge mandatory">VPN Only</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderLinked(): TemplateResult {
|
||||||
|
const meta = this.route?.metadata;
|
||||||
|
if (!meta) return html``;
|
||||||
|
const hasProfile = !!meta.sourceProfileName;
|
||||||
|
const hasTarget = !!meta.networkTargetName;
|
||||||
|
if (!hasProfile && !hasTarget) return html``;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="section linked">
|
||||||
|
<div class="section-label">Linked</div>
|
||||||
|
${hasProfile
|
||||||
|
? html`
|
||||||
|
<div class="field-row">
|
||||||
|
<span class="field-key">Profile</span>
|
||||||
|
<span class="field-value"><span class="linked-name">${meta.sourceProfileName}</span></span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
|
${hasTarget
|
||||||
|
? html`
|
||||||
|
<div class="field-row">
|
||||||
|
<span class="field-key">Target</span>
|
||||||
|
<span class="field-value"><span class="linked-name">${meta.networkTargetName}</span></span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
private renderFeatures(): TemplateResult {
|
private renderFeatures(): TemplateResult {
|
||||||
if (!this.route) return html``;
|
if (!this.route) return html``;
|
||||||
const features: TemplateResult[] = [];
|
const features: TemplateResult[] = [];
|
||||||
const action = this.route.action;
|
const action = this.route.action;
|
||||||
const security = this.route.security;
|
const security = this.route.security;
|
||||||
const headers = this.route.headers;
|
const headers = this.route.headers;
|
||||||
|
const meta = this.route.metadata;
|
||||||
|
|
||||||
if (action.tls) {
|
if (action.tls) {
|
||||||
features.push(html`<span class="feature"><span class="feature-icon">🔒</span>TLS</span>`);
|
features.push(html`<span class="feature"><span class="feature-icon">🔒</span>TLS</span>`);
|
||||||
@@ -660,6 +866,12 @@ export class SzRouteCard extends DeesElement {
|
|||||||
if (headers) {
|
if (headers) {
|
||||||
features.push(html`<span class="feature"><span class="feature-icon">⚙</span>Headers</span>`);
|
features.push(html`<span class="feature"><span class="feature-icon">⚙</span>Headers</span>`);
|
||||||
}
|
}
|
||||||
|
if (this.route?.vpnOnly) {
|
||||||
|
features.push(html`<span class="feature"><span class="feature-icon">🔐</span>VPN</span>`);
|
||||||
|
}
|
||||||
|
if (meta?.sourceProfileName || meta?.networkTargetName) {
|
||||||
|
features.push(html`<span class="feature"><span class="feature-icon">🔗</span>Linked</span>`);
|
||||||
|
}
|
||||||
|
|
||||||
if (features.length === 0) return html``;
|
if (features.length === 0) return html``;
|
||||||
return html`<div class="features-row">${features}</div>`;
|
return html`<div class="features-row">${features}</div>`;
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ export class SzRouteListView extends DeesElement {
|
|||||||
@property({ type: Array })
|
@property({ type: Array })
|
||||||
public accessor routes: IRouteConfig[] = [];
|
public accessor routes: IRouteConfig[] = [];
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public accessor showActionsFilter: ((route: IRouteConfig) => boolean) | null = null;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private accessor searchQuery: string = '';
|
private accessor searchQuery: string = '';
|
||||||
|
|
||||||
@@ -299,6 +302,7 @@ export class SzRouteListView extends DeesElement {
|
|||||||
(route) => html`
|
(route) => html`
|
||||||
<sz-route-card
|
<sz-route-card
|
||||||
.route=${route}
|
.route=${route}
|
||||||
|
.showActions=${this.showActionsFilter?.(route) ?? false}
|
||||||
@click=${() => this.handleRouteClick(route)}
|
@click=${() => this.handleRouteClick(route)}
|
||||||
></sz-route-card>
|
></sz-route-card>
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ import {
|
|||||||
|
|
||||||
import type { IExecutionEnvironment } from '@design.estate/dees-catalog';
|
import type { IExecutionEnvironment } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
import './sz-stat-card.js';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'sz-service-detail-view': SzServiceDetailView;
|
'sz-service-detail-view': SzServiceDetailView;
|
||||||
@@ -218,31 +216,97 @@ export class SzServiceDetailView extends DeesElement {
|
|||||||
gap: 24px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card-header {
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
height: 36px;
|
||||||
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
display: flex;
|
||||||
border-radius: 8px;
|
align-items: center;
|
||||||
overflow: hidden;
|
padding: 0 8px 0 16px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-heading {
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
align-items: baseline;
|
||||||
align-items: center;
|
gap: 8px;
|
||||||
padding: 16px;
|
min-width: 0;
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
font-size: 16px;
|
font-weight: 500;
|
||||||
font-weight: 600;
|
font-size: 13px;
|
||||||
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--dees-color-text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-subtitle {
|
.card-subtitle {
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
color: var(--dees-color-text-muted);
|
||||||
margin-top: 2px;
|
letter-spacing: -0.01em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
height: 36px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button {
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid var(--dees-color-border-subtle);
|
||||||
|
color: var(--dees-color-text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button:first-child {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button:hover {
|
||||||
|
background: var(--dees-color-hover);
|
||||||
|
color: var(--dees-color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.primary {
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.primary:hover {
|
||||||
|
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.danger {
|
||||||
|
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.danger:hover {
|
||||||
|
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.1)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-content {
|
.card-content {
|
||||||
@@ -396,68 +460,6 @@ export class SzServiceDetailView extends DeesElement {
|
|||||||
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stream-button {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
background: ${cssManager.bdTheme('#2563eb', '#3b82f6')};
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: white;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stream-button.streaming {
|
|
||||||
background: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-button {
|
|
||||||
padding: 6px 12px;
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs-container {
|
|
||||||
padding: 16px;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-entry {
|
|
||||||
padding: 2px 0;
|
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-logs {
|
|
||||||
padding: 24px;
|
|
||||||
text-align: center;
|
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-badge {
|
.tag-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
@@ -580,12 +582,11 @@ export class SzServiceDetailView extends DeesElement {
|
|||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<div class="card">
|
<dees-tile>
|
||||||
<div class="card-header">
|
<div slot="header" class="card-header">
|
||||||
<div>
|
<div class="card-heading">
|
||||||
<div class="card-title">Service Details</div>
|
<span class="card-title">Service Details</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="action-button" style="width: auto; padding: 6px 12px;" @click=${() => this.handleEdit()}>Edit</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="detail-list">
|
<div class="detail-list">
|
||||||
@@ -619,43 +620,30 @@ export class SzServiceDetailView extends DeesElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div slot="footer" class="card-footer">
|
||||||
|
<button class="tile-button primary" @click=${() => this.handleEdit()}>Edit</button>
|
||||||
|
</div>
|
||||||
|
</dees-tile>
|
||||||
|
|
||||||
<div class="card">
|
<dees-chart-log
|
||||||
<div class="card-header">
|
.label=${'Service Logs'}
|
||||||
<div class="logs-header" style="width: 100%;">
|
.logEntries=${this.logs.map(log => ({
|
||||||
<div>
|
timestamp: log.timestamp?.includes?.('T') ? log.timestamp : new Date(log.timestamp || Date.now()).toISOString(),
|
||||||
<div class="card-title">Logs</div>
|
level: (log.level || 'info') as 'debug' | 'info' | 'warn' | 'error',
|
||||||
<div class="card-subtitle">Container logs</div>
|
message: log.message,
|
||||||
</div>
|
}))}
|
||||||
<div class="logs-actions">
|
.autoScroll=${true}
|
||||||
<button class="stream-button ${this.streaming ? 'streaming' : ''}" @click=${() => this.toggleStreaming()}>
|
.maxEntries=${2000}
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
.showMetrics=${true}
|
||||||
${this.streaming
|
></dees-chart-log>
|
||||||
? html`<rect x="6" y="6" width="12" height="12" rx="1"/>`
|
|
||||||
: html`<polygon points="5,3 19,12 5,21"/>`
|
|
||||||
}
|
|
||||||
</svg>
|
|
||||||
${this.streaming ? 'Stop' : 'Stream'}
|
|
||||||
</button>
|
|
||||||
<button class="clear-button" @click=${() => this.handleClearLogs()}>Clear logs</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="logs-container">
|
|
||||||
${this.logs.length > 0 ? this.logs.map(log => html`
|
|
||||||
<div class="log-entry">${log.timestamp} ${log.message}</div>
|
|
||||||
`) : html`
|
|
||||||
<div class="empty-logs">Click "Stream" to start live log streaming</div>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div class="card">
|
<dees-tile>
|
||||||
<div class="card-header">
|
<div slot="header" class="card-header">
|
||||||
<div class="card-title">Live stats</div>
|
<div class="card-heading">
|
||||||
|
<span class="card-title">Live stats</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
@@ -678,13 +666,13 @@ export class SzServiceDetailView extends DeesElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dees-tile>
|
||||||
|
|
||||||
<div class="card">
|
<dees-tile>
|
||||||
<div class="card-header">
|
<div slot="header" class="card-header">
|
||||||
<div>
|
<div class="card-heading">
|
||||||
<div class="card-title">Actions</div>
|
<span class="card-title">Actions</span>
|
||||||
<div class="card-subtitle">Manage service state</div>
|
<span class="card-subtitle">Manage service state</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
@@ -704,13 +692,13 @@ export class SzServiceDetailView extends DeesElement {
|
|||||||
<button class="action-button danger" @click=${() => this.handleAction('delete')}>Delete Service</button>
|
<button class="action-button danger" @click=${() => this.handleAction('delete')}>Delete Service</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dees-tile>
|
||||||
|
|
||||||
<div class="card">
|
<dees-tile>
|
||||||
<div class="card-header">
|
<div slot="header" class="card-header">
|
||||||
<div>
|
<div class="card-heading">
|
||||||
<div class="card-title">Image Source</div>
|
<span class="card-title">Image Source</span>
|
||||||
<div class="card-subtitle">${this.service.registry === 'Docker Hub' ? 'External container registry' : 'Onebox registry'}</div>
|
<span class="card-subtitle">${this.service.registry === 'Docker Hub' ? 'External container registry' : 'Onebox registry'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
@@ -729,21 +717,14 @@ export class SzServiceDetailView extends DeesElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dees-tile>
|
||||||
|
|
||||||
<div class="card">
|
<dees-tile>
|
||||||
<div class="card-header">
|
<div slot="header" class="card-header">
|
||||||
<div>
|
<div class="card-heading">
|
||||||
<div class="card-title">Backups</div>
|
<span class="card-title">Backups</span>
|
||||||
<div class="card-subtitle">Create and manage service backups</div>
|
<span class="card-subtitle">Create and manage service backups</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="action-button" style="width: auto; padding: 6px 12px;" @click=${() => this.handleCreateBackup()}>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px;">
|
|
||||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
||||||
</svg>
|
|
||||||
Create Backup
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="backup-list">
|
<div class="backup-list">
|
||||||
@@ -778,7 +759,16 @@ export class SzServiceDetailView extends DeesElement {
|
|||||||
`)}
|
`)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div slot="footer" class="card-footer">
|
||||||
|
<button class="tile-button primary" @click=${() => this.handleCreateBackup()}>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
Create Backup
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</dees-tile>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -65,71 +65,94 @@ export class SzServicesBackupsView extends DeesElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
dees-tile {
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
display: block;
|
||||||
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
|
height: 36px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
align-items: center;
|
||||||
align-items: flex-start;
|
padding: 0 16px;
|
||||||
padding: 16px;
|
width: 100%;
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-info {
|
.section-heading {
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: baseline;
|
||||||
gap: 4px;
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 16px;
|
font-weight: 500;
|
||||||
font-weight: 600;
|
font-size: 13px;
|
||||||
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--dees-color-text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-subtitle {
|
.section-subtitle {
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
color: var(--dees-color-text-muted);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions {
|
.section-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
height: 36px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button {
|
.tile-button {
|
||||||
display: inline-flex;
|
padding: 0 16px;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid var(--dees-color-border-subtle);
|
||||||
|
color: var(--dees-color-text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 8px 14px;
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 200ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button:hover {
|
.tile-button:first-child {
|
||||||
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
border-left: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button.primary {
|
.tile-button:hover {
|
||||||
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
background: var(--dees-color-hover);
|
||||||
color: ${cssManager.bdTheme('#fafafa', '#18181b')};
|
color: var(--dees-color-text-primary);
|
||||||
border: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button.primary:hover {
|
.tile-button.primary {
|
||||||
opacity: 0.9;
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.primary:hover {
|
||||||
|
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-header {
|
.table-header {
|
||||||
@@ -253,28 +276,11 @@ export class SzServicesBackupsView extends DeesElement {
|
|||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="section">
|
<dees-tile>
|
||||||
<div class="section-header">
|
<div slot="header" class="section-header">
|
||||||
<div class="section-info">
|
<div class="section-heading">
|
||||||
<div class="section-title">Backup Schedules</div>
|
<span class="section-title">Backup Schedules</span>
|
||||||
<div class="section-subtitle">Configure automated backup schedules for your services</div>
|
<span class="section-subtitle">Configure automated backup schedules for your services</span>
|
||||||
</div>
|
|
||||||
<div class="header-actions">
|
|
||||||
<button class="action-button" @click=${() => this.handleImport()}>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
||||||
<polyline points="17 8 12 3 7 8"/>
|
|
||||||
<line x1="12" y1="3" x2="12" y2="15"/>
|
|
||||||
</svg>
|
|
||||||
Import Backup
|
|
||||||
</button>
|
|
||||||
<button class="action-button primary" @click=${() => this.handleCreateSchedule()}>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
||||||
</svg>
|
|
||||||
Create Schedule
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-header schedules-header">
|
<div class="table-header schedules-header">
|
||||||
@@ -318,13 +324,30 @@ export class SzServicesBackupsView extends DeesElement {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
`)}
|
`)}
|
||||||
</div>
|
<div slot="footer" class="section-footer">
|
||||||
|
<button class="tile-button" @click=${() => this.handleImport()}>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||||
|
<polyline points="17 8 12 3 7 8"/>
|
||||||
|
<line x1="12" y1="3" x2="12" y2="15"/>
|
||||||
|
</svg>
|
||||||
|
Import Backup
|
||||||
|
</button>
|
||||||
|
<button class="tile-button primary" @click=${() => this.handleCreateSchedule()}>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
Create Schedule
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</dees-tile>
|
||||||
|
|
||||||
<div class="section">
|
<dees-tile>
|
||||||
<div class="section-header">
|
<div slot="header" class="section-header">
|
||||||
<div class="section-info">
|
<div class="section-heading">
|
||||||
<div class="section-title">All Backups</div>
|
<span class="section-title">All Backups</span>
|
||||||
<div class="section-subtitle">Browse and manage all backups across services</div>
|
<span class="section-subtitle">Browse and manage all backups across services</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-header backups-header">
|
<div class="table-header backups-header">
|
||||||
@@ -358,7 +381,7 @@ export class SzServicesBackupsView extends DeesElement {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
`)}
|
`)}
|
||||||
</div>
|
</dees-tile>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+216
-156
@@ -72,28 +72,98 @@ export class SzSettingsView extends DeesElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
dees-tile {
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
display: block;
|
||||||
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
margin-bottom: 16px;
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 16px;
|
font-weight: 500;
|
||||||
font-weight: 600;
|
font-size: 13px;
|
||||||
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--dees-color-text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-subtitle {
|
.section-subtitle {
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
color: var(--dees-color-text-muted);
|
||||||
margin-top: 2px;
|
letter-spacing: -0.01em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
height: 36px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button {
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid var(--dees-color-border-subtle);
|
||||||
|
color: var(--dees-color-text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button:first-child {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button:hover {
|
||||||
|
background: var(--dees-color-hover);
|
||||||
|
color: var(--dees-color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.primary {
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.primary:hover {
|
||||||
|
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
@@ -224,161 +294,151 @@ export class SzSettingsView extends DeesElement {
|
|||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
padding-top: 16px;
|
|
||||||
border-top: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 200ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.secondary {
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.secondary:hover {
|
|
||||||
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.primary {
|
|
||||||
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
|
||||||
border: none;
|
|
||||||
color: ${cssManager.bdTheme('#fafafa', '#18181b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.primary:hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="section">
|
<dees-tile>
|
||||||
<div class="section-header">
|
<div slot="header" class="section-header">
|
||||||
<div class="section-title">Appearance</div>
|
<div class="section-heading">
|
||||||
<div class="section-subtitle">Customize the look and feel</div>
|
<span class="section-title">Appearance</span>
|
||||||
</div>
|
<span class="section-subtitle">Customize the look and feel</span>
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-label-group">
|
|
||||||
<span class="form-label">Dark Mode</span>
|
|
||||||
<span class="form-hint">Toggle dark mode on or off</span>
|
|
||||||
</div>
|
|
||||||
<div class="toggle-switch ${this.settings.darkMode ? 'active' : ''}" @click=${() => this.toggleDarkMode()}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">Cloudflare Integration</div>
|
|
||||||
<div class="section-subtitle">Configure Cloudflare API for DNS management</div>
|
|
||||||
</div>
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="field-label">API Token</div>
|
|
||||||
<input type="password" placeholder="Enter Cloudflare API token" .value=${this.settings.cloudflareToken} @input=${(e: Event) => this.updateSetting('cloudflareToken', (e.target as HTMLInputElement).value)}>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="field-label">Zone ID (Optional)</div>
|
|
||||||
<input type="text" placeholder="Default zone ID" .value=${this.settings.cloudflareZoneId} @input=${(e: Event) => this.updateSetting('cloudflareZoneId', (e.target as HTMLInputElement).value)}>
|
|
||||||
</div>
|
|
||||||
<div class="form-hint">Get your API token from the Cloudflare dashboard with DNS edit permissions.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">SSL/TLS Settings</div>
|
|
||||||
<div class="section-subtitle">Configure certificate management</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-label-group">
|
|
||||||
<span class="form-label">Auto-Renew Certificates</span>
|
|
||||||
<span class="form-hint">Automatically renew certificates before expiry</span>
|
|
||||||
</div>
|
|
||||||
<div class="toggle-switch ${this.settings.autoRenewCerts ? 'active' : ''}" @click=${() => this.toggleSetting('autoRenewCerts')}></div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-top: 16px;">
|
|
||||||
<div class="field-label">Renewal Threshold (days)</div>
|
|
||||||
<input type="number" .value=${String(this.settings.renewalThreshold)} @input=${(e: Event) => this.updateSetting('renewalThreshold', parseInt((e.target as HTMLInputElement).value))}>
|
|
||||||
<div class="form-hint">Renew certificates when they have fewer than this many days remaining.</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="field-label">ACME Email</div>
|
|
||||||
<input type="email" placeholder="admin@example.com" .value=${this.settings.acmeEmail} @input=${(e: Event) => this.updateSetting('acmeEmail', (e.target as HTMLInputElement).value)}>
|
|
||||||
<div class="form-hint">Email address for Let's Encrypt notifications.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">Network Settings</div>
|
|
||||||
<div class="section-subtitle">Configure network and proxy settings</div>
|
|
||||||
</div>
|
|
||||||
<div class="input-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="field-label">HTTP Port</div>
|
|
||||||
<input type="number" .value=${String(this.settings.httpPort)} @input=${(e: Event) => this.updateSetting('httpPort', parseInt((e.target as HTMLInputElement).value))}>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="field-label">HTTPS Port</div>
|
|
||||||
<input type="number" .value=${String(this.settings.httpsPort)} @input=${(e: Event) => this.updateSetting('httpsPort', parseInt((e.target as HTMLInputElement).value))}>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="section-content">
|
||||||
<div class="form-label-group">
|
<div class="form-row">
|
||||||
<span class="form-label">Force HTTPS</span>
|
<div class="form-label-group">
|
||||||
<span class="form-hint">Redirect all HTTP traffic to HTTPS</span>
|
<span class="form-label">Dark Mode</span>
|
||||||
</div>
|
<span class="form-hint">Toggle dark mode on or off</span>
|
||||||
<div class="toggle-switch ${this.settings.forceHttps ? 'active' : ''}" @click=${() => this.toggleSetting('forceHttps')}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">Account</div>
|
|
||||||
<div class="section-subtitle">Manage your account settings</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="field-label">Current User</div>
|
|
||||||
<div style="font-size: 14px; color: ${cssManager.bdTheme('#18181b', '#fafafa')};">${this.currentUser || 'Unknown'}</div>
|
|
||||||
</div>
|
|
||||||
<div class="password-section">
|
|
||||||
<div class="password-title">Change Password</div>
|
|
||||||
<div class="password-fields">
|
|
||||||
<div>
|
|
||||||
<div class="field-label">Current Password</div>
|
|
||||||
<input type="password" id="currentPassword">
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="toggle-switch ${this.settings.darkMode ? 'active' : ''}" @click=${() => this.toggleDarkMode()}></div>
|
||||||
<div class="field-label">New Password</div>
|
|
||||||
<input type="password" id="newPassword">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="field-label">Confirm Password</div>
|
|
||||||
<input type="password" id="confirmPassword">
|
|
||||||
</div>
|
|
||||||
<button class="button secondary" style="width: fit-content;" @click=${() => this.handleChangePassword()}>Update Password</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dees-tile>
|
||||||
|
|
||||||
<div class="actions">
|
<dees-tile>
|
||||||
<button class="button secondary" @click=${() => this.handleReset()}>Reset</button>
|
<div slot="header" class="section-header">
|
||||||
<button class="button primary" @click=${() => this.handleSave()}>Save Settings</button>
|
<div class="section-heading">
|
||||||
</div>
|
<span class="section-title">Cloudflare Integration</span>
|
||||||
|
<span class="section-subtitle">Configure Cloudflare API for DNS management</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="field-label">API Token</div>
|
||||||
|
<input type="password" placeholder="Enter Cloudflare API token" .value=${this.settings.cloudflareToken} @input=${(e: Event) => this.updateSetting('cloudflareToken', (e.target as HTMLInputElement).value)}>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="field-label">Zone ID (Optional)</div>
|
||||||
|
<input type="text" placeholder="Default zone ID" .value=${this.settings.cloudflareZoneId} @input=${(e: Event) => this.updateSetting('cloudflareZoneId', (e.target as HTMLInputElement).value)}>
|
||||||
|
</div>
|
||||||
|
<div class="form-hint">Get your API token from the Cloudflare dashboard with DNS edit permissions.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dees-tile>
|
||||||
|
|
||||||
|
<dees-tile>
|
||||||
|
<div slot="header" class="section-header">
|
||||||
|
<div class="section-heading">
|
||||||
|
<span class="section-title">SSL/TLS Settings</span>
|
||||||
|
<span class="section-subtitle">Configure certificate management</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-label-group">
|
||||||
|
<span class="form-label">Auto-Renew Certificates</span>
|
||||||
|
<span class="form-hint">Automatically renew certificates before expiry</span>
|
||||||
|
</div>
|
||||||
|
<div class="toggle-switch ${this.settings.autoRenewCerts ? 'active' : ''}" @click=${() => this.toggleSetting('autoRenewCerts')}></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-top: 16px;">
|
||||||
|
<div class="field-label">Renewal Threshold (days)</div>
|
||||||
|
<input type="number" .value=${String(this.settings.renewalThreshold)} @input=${(e: Event) => this.updateSetting('renewalThreshold', parseInt((e.target as HTMLInputElement).value))}>
|
||||||
|
<div class="form-hint">Renew certificates when they have fewer than this many days remaining.</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="field-label">ACME Email</div>
|
||||||
|
<input type="email" placeholder="admin@example.com" .value=${this.settings.acmeEmail} @input=${(e: Event) => this.updateSetting('acmeEmail', (e.target as HTMLInputElement).value)}>
|
||||||
|
<div class="form-hint">Email address for Let's Encrypt notifications.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dees-tile>
|
||||||
|
|
||||||
|
<dees-tile>
|
||||||
|
<div slot="header" class="section-header">
|
||||||
|
<div class="section-heading">
|
||||||
|
<span class="section-title">Network Settings</span>
|
||||||
|
<span class="section-subtitle">Configure network and proxy settings</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
<div class="input-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="field-label">HTTP Port</div>
|
||||||
|
<input type="number" .value=${String(this.settings.httpPort)} @input=${(e: Event) => this.updateSetting('httpPort', parseInt((e.target as HTMLInputElement).value))}>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="field-label">HTTPS Port</div>
|
||||||
|
<input type="number" .value=${String(this.settings.httpsPort)} @input=${(e: Event) => this.updateSetting('httpsPort', parseInt((e.target as HTMLInputElement).value))}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-label-group">
|
||||||
|
<span class="form-label">Force HTTPS</span>
|
||||||
|
<span class="form-hint">Redirect all HTTP traffic to HTTPS</span>
|
||||||
|
</div>
|
||||||
|
<div class="toggle-switch ${this.settings.forceHttps ? 'active' : ''}" @click=${() => this.toggleSetting('forceHttps')}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dees-tile>
|
||||||
|
|
||||||
|
<dees-tile>
|
||||||
|
<div slot="header" class="section-header">
|
||||||
|
<div class="section-heading">
|
||||||
|
<span class="section-title">Account</span>
|
||||||
|
<span class="section-subtitle">Manage your account settings</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="field-label">Current User</div>
|
||||||
|
<div style="font-size: 14px; color: ${cssManager.bdTheme('#18181b', '#fafafa')};">${this.currentUser || 'Unknown'}</div>
|
||||||
|
</div>
|
||||||
|
<div class="password-section">
|
||||||
|
<div class="password-title">Change Password</div>
|
||||||
|
<div class="password-fields">
|
||||||
|
<div>
|
||||||
|
<div class="field-label">Current Password</div>
|
||||||
|
<input type="password" id="currentPassword">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="field-label">New Password</div>
|
||||||
|
<input type="password" id="newPassword">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="field-label">Confirm Password</div>
|
||||||
|
<input type="password" id="confirmPassword">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div slot="footer" class="section-footer">
|
||||||
|
<button class="tile-button" @click=${() => this.handleChangePassword()}>Update Password</button>
|
||||||
|
</div>
|
||||||
|
</dees-tile>
|
||||||
|
|
||||||
|
<dees-tile>
|
||||||
|
<div class="section-content" style="padding: 12px 16px; text-align: center; color: var(--dees-color-text-muted); font-size: 12px;">
|
||||||
|
Save your changes or reset to defaults.
|
||||||
|
</div>
|
||||||
|
<div slot="footer" class="section-footer">
|
||||||
|
<button class="tile-button" @click=${() => this.handleReset()}>Reset</button>
|
||||||
|
<button class="tile-button primary" @click=${() => this.handleSave()}>Save Settings</button>
|
||||||
|
</div>
|
||||||
|
</dees-tile>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,189 +0,0 @@
|
|||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
customElement,
|
|
||||||
html,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
property,
|
|
||||||
type TemplateResult,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'sz-stat-card': SzStatCard;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement('sz-stat-card')
|
|
||||||
export class SzStatCard extends DeesElement {
|
|
||||||
public static demo = () => html`
|
|
||||||
<style>
|
|
||||||
.demo-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
padding: 24px;
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="demo-grid">
|
|
||||||
<sz-stat-card
|
|
||||||
label="Total Services"
|
|
||||||
value="7"
|
|
||||||
icon="server"
|
|
||||||
></sz-stat-card>
|
|
||||||
<sz-stat-card
|
|
||||||
label="Running"
|
|
||||||
value="7"
|
|
||||||
icon="check"
|
|
||||||
variant="success"
|
|
||||||
></sz-stat-card>
|
|
||||||
<sz-stat-card
|
|
||||||
label="Stopped"
|
|
||||||
value="0"
|
|
||||||
icon="stop"
|
|
||||||
></sz-stat-card>
|
|
||||||
<sz-stat-card
|
|
||||||
label="Docker"
|
|
||||||
value="Running"
|
|
||||||
icon="container"
|
|
||||||
variant="success"
|
|
||||||
valueBadge
|
|
||||||
></sz-stat-card>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
public static demoGroups = ['Dashboard'];
|
|
||||||
|
|
||||||
@property({ type: String })
|
|
||||||
public accessor label: string = '';
|
|
||||||
|
|
||||||
@property({ type: String })
|
|
||||||
public accessor value: string = '';
|
|
||||||
|
|
||||||
@property({ type: String })
|
|
||||||
public accessor icon: string = '';
|
|
||||||
|
|
||||||
@property({ type: String })
|
|
||||||
public accessor variant: 'default' | 'success' | 'warning' | 'error' = 'default';
|
|
||||||
|
|
||||||
@property({ type: Boolean })
|
|
||||||
public accessor valueBadge: boolean = false;
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
transition: all 200ms ease;
|
|
||||||
height: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
|
|
||||||
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.05)', 'rgba(0,0,0,0.2)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value.success {
|
|
||||||
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.value.warning {
|
|
||||||
color: ${cssManager.bdTheme('#ca8a04', '#facc15')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.value.error {
|
|
||||||
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 9999px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge.success {
|
|
||||||
background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
|
|
||||||
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge.warning {
|
|
||||||
background: ${cssManager.bdTheme('#fef9c3', 'rgba(250, 204, 21, 0.2)')};
|
|
||||||
color: ${cssManager.bdTheme('#ca8a04', '#facc15')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge.error {
|
|
||||||
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
|
|
||||||
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge.default {
|
|
||||||
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
public render(): TemplateResult {
|
|
||||||
const valueClass = this.valueBadge ? `badge ${this.variant}` : `value ${this.variant}`;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="card">
|
|
||||||
<div class="header">
|
|
||||||
<span class="label">${this.label}</span>
|
|
||||||
${this.renderIcon()}
|
|
||||||
</div>
|
|
||||||
<div class="${valueClass}">${this.value}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderIcon(): TemplateResult {
|
|
||||||
const icons: Record<string, TemplateResult> = {
|
|
||||||
server: html`<svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>`,
|
|
||||||
check: html`<svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>`,
|
|
||||||
stop: html`<svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="10" y1="15" x2="10" y2="9"></line><line x1="14" y1="15" x2="14" y2="9"></line></svg>`,
|
|
||||||
container: html`<svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path></svg>`,
|
|
||||||
};
|
|
||||||
|
|
||||||
return icons[this.icon] || html``;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,8 +7,7 @@ import {
|
|||||||
property,
|
property,
|
||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
import type { IStatsTile } from '@design.estate/dees-catalog';
|
||||||
import './sz-stat-card.js';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -54,54 +53,51 @@ export class SzStatusGridCluster extends DeesElement {
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid > * {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.grid {
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private get tiles(): IStatsTile[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'total',
|
||||||
|
title: 'Total Services',
|
||||||
|
value: this.stats.totalServices,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:server',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'running',
|
||||||
|
title: 'Running',
|
||||||
|
value: this.stats.running,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:check',
|
||||||
|
color: '#22c55e',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'stopped',
|
||||||
|
title: 'Stopped',
|
||||||
|
value: this.stats.stopped,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:circleStop',
|
||||||
|
color: this.stats.stopped > 0 ? '#f59e0b' : undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'docker',
|
||||||
|
title: 'Docker',
|
||||||
|
value: this.stats.dockerStatus === 'running' ? 'Running' : 'Stopped',
|
||||||
|
type: 'text',
|
||||||
|
icon: 'lucide:container',
|
||||||
|
color: this.stats.dockerStatus === 'running' ? '#22c55e' : '#ef4444',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="grid">
|
<dees-statsgrid
|
||||||
<sz-stat-card
|
.tiles=${this.tiles}
|
||||||
label="Total Services"
|
.minTileWidth=${200}
|
||||||
value="${this.stats.totalServices}"
|
></dees-statsgrid>
|
||||||
icon="server"
|
|
||||||
></sz-stat-card>
|
|
||||||
<sz-stat-card
|
|
||||||
label="Running"
|
|
||||||
value="${this.stats.running}"
|
|
||||||
icon="check"
|
|
||||||
variant="success"
|
|
||||||
></sz-stat-card>
|
|
||||||
<sz-stat-card
|
|
||||||
label="Stopped"
|
|
||||||
value="${this.stats.stopped}"
|
|
||||||
icon="stop"
|
|
||||||
variant="${this.stats.stopped > 0 ? 'warning' : 'default'}"
|
|
||||||
></sz-stat-card>
|
|
||||||
<sz-stat-card
|
|
||||||
label="Docker"
|
|
||||||
value="${this.stats.dockerStatus === 'running' ? 'Running' : 'Stopped'}"
|
|
||||||
icon="container"
|
|
||||||
variant="${this.stats.dockerStatus === 'running' ? 'success' : 'error'}"
|
|
||||||
valueBadge
|
|
||||||
></sz-stat-card>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export class SzStatusGridServices extends DeesElement {
|
|||||||
return html`
|
return html`
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<sz-resource-usage-card
|
<sz-resource-usage-card
|
||||||
.usage=${this.resourceUsage}
|
.data=${this.resourceUsage}
|
||||||
></sz-resource-usage-card>
|
></sz-resource-usage-card>
|
||||||
<sz-platform-services-card
|
<sz-platform-services-card
|
||||||
.services=${this.platformServices}
|
.services=${this.platformServices}
|
||||||
|
|||||||
@@ -55,56 +55,94 @@ export class SzTokensView extends DeesElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
dees-tile {
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
display: block;
|
||||||
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
|
height: 36px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
align-items: center;
|
||||||
align-items: flex-start;
|
padding: 0 16px;
|
||||||
padding: 16px;
|
width: 100%;
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-info {
|
.section-heading {
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: baseline;
|
||||||
gap: 4px;
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 16px;
|
font-weight: 500;
|
||||||
font-weight: 600;
|
font-size: 13px;
|
||||||
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--dees-color-text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-subtitle {
|
.section-subtitle {
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
color: var(--dees-color-text-muted);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-button {
|
.section-footer {
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
height: 36px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button {
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid var(--dees-color-border-subtle);
|
||||||
|
color: var(--dees-color-text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 8px 14px;
|
|
||||||
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${cssManager.bdTheme('#fafafa', '#18181b')};
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 200ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-button:hover {
|
.tile-button:first-child {
|
||||||
opacity: 0.9;
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button:hover {
|
||||||
|
background: var(--dees-color-hover);
|
||||||
|
color: var(--dees-color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.primary {
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-button.primary:hover {
|
||||||
|
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.token-list {
|
.token-list {
|
||||||
@@ -192,45 +230,18 @@ export class SzTokensView extends DeesElement {
|
|||||||
.empty-text {
|
.empty-text {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-button {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 200ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-button:hover {
|
|
||||||
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="section">
|
<dees-tile>
|
||||||
<div class="section-header">
|
<div slot="header" class="section-header">
|
||||||
<div class="section-info">
|
<div class="section-heading">
|
||||||
<div class="section-title">Global Tokens</div>
|
<span class="section-title">Global Tokens</span>
|
||||||
<div class="section-subtitle">Tokens that can push images to multiple services</div>
|
<span class="section-subtitle">Tokens that can push images to multiple services</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="create-button" @click=${() => this.handleCreate('global')}>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
||||||
</svg>
|
|
||||||
Create Token
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
${this.globalTokens.length > 0 ? html`
|
${this.globalTokens.length > 0 ? html`
|
||||||
<div class="token-list">
|
<div class="token-list">
|
||||||
@@ -239,25 +250,26 @@ export class SzTokensView extends DeesElement {
|
|||||||
` : html`
|
` : html`
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-text">No global tokens created</div>
|
<div class="empty-text">No global tokens created</div>
|
||||||
<button class="empty-button" @click=${() => this.handleCreate('global')}>Create Global Token</button>
|
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
</div>
|
<div slot="footer" class="section-footer">
|
||||||
|
<button class="tile-button primary" @click=${() => this.handleCreate('global')}>
|
||||||
<div class="section">
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-info">
|
|
||||||
<div class="section-title">CI Tokens (Service-specific)</div>
|
|
||||||
<div class="section-subtitle">Tokens tied to individual services for CI/CD pipelines</div>
|
|
||||||
</div>
|
|
||||||
<button class="create-button" @click=${() => this.handleCreate('ci')}>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
</svg>
|
</svg>
|
||||||
Create Token
|
Create Token
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</dees-tile>
|
||||||
|
|
||||||
|
<dees-tile>
|
||||||
|
<div slot="header" class="section-header">
|
||||||
|
<div class="section-heading">
|
||||||
|
<span class="section-title">CI Tokens (Service-specific)</span>
|
||||||
|
<span class="section-subtitle">Tokens tied to individual services for CI/CD pipelines</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
${this.ciTokens.length > 0 ? html`
|
${this.ciTokens.length > 0 ? html`
|
||||||
<div class="token-list">
|
<div class="token-list">
|
||||||
${this.ciTokens.map(token => this.renderToken(token))}
|
${this.ciTokens.map(token => this.renderToken(token))}
|
||||||
@@ -265,10 +277,18 @@ export class SzTokensView extends DeesElement {
|
|||||||
` : html`
|
` : html`
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-text">No CI tokens created</div>
|
<div class="empty-text">No CI tokens created</div>
|
||||||
<button class="empty-button" @click=${() => this.handleCreate('ci')}>Create CI Token</button>
|
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
</div>
|
<div slot="footer" class="section-footer">
|
||||||
|
<button class="tile-button primary" @click=${() => this.handleCreate('ci')}>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
Create Token
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</dees-tile>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"skipLibCheck": true
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"dist_*/**/*.d.ts"
|
"dist_*/**/*.d.ts"
|
||||||
|
|||||||
Reference in New Issue
Block a user