Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70c43ceae3 | |||
| cf247117d4 | |||
| 35ffde9253 | |||
| 531ec5a2d0 | |||
| cde6ccd841 | |||
| f181f662d8 | |||
| 85f26c69d6 | |||
| 9d1471a363 | |||
| 6db2e3ff4f | |||
| bbbc0958f4 | |||
| 52c9c7251e | |||
| a1409a4d57 | |||
| 61359bc712 | |||
| 889f84d666 | |||
| a188fcbe85 |
@@ -2,6 +2,17 @@
|
|||||||
"@git.zone/tswatch": {
|
"@git.zone/tswatch": {
|
||||||
"preset": "element"
|
"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": {
|
"@git.zone/cli": {
|
||||||
"projectType": "wcc",
|
"projectType": "wcc",
|
||||||
"module": {
|
"module": {
|
||||||
48
changelog.md
48
changelog.md
@@ -1,5 +1,53 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
|||||||
21
license
Normal file
21
license
Normal file
@@ -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.
|
||||||
27
package.json
27
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/catalog",
|
"name": "@serve.zone/catalog",
|
||||||
"version": "2.6.1",
|
"version": "2.11.0",
|
||||||
"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": "Lossless GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@design.estate/dees-catalog": "^3.43.0",
|
"@design.estate/dees-catalog": "^3.49.2",
|
||||||
"@design.estate/dees-domtools": "^2.3.8",
|
"@design.estate/dees-domtools": "^2.5.4",
|
||||||
"@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.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^4.1.2",
|
"@git.zone/tsbuild": "^4.4.0",
|
||||||
"@git.zone/tsbundle": "^2.8.3",
|
"@git.zone/tsbundle": "^2.10.0",
|
||||||
"@git.zone/tsrun": "^2.0.1",
|
"@git.zone/tsrun": "^2.0.2",
|
||||||
"@git.zone/tstest": "^3.1.8",
|
"@git.zone/tstest": "^3.6.3",
|
||||||
"@git.zone/tswatch": "^3.1.0",
|
"@git.zone/tswatch": "^3.3.2",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.1.0",
|
||||||
"@types/node": "^25.3.0"
|
"@types/node": "^25.5.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": [
|
||||||
|
|||||||
3229
pnpm-lock.yaml
generated
3229
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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 (34 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
|
||||||
|
|
||||||
@@ -20,9 +20,14 @@
|
|||||||
| 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)
|
||||||
|
|||||||
49
readme.md
49
readme.md
@@ -14,15 +14,19 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
|
|
||||||
## 🚀 What It Does
|
## 🚀 What It Does
|
||||||
|
|
||||||
`@serve.zone/catalog` provides **30+ production-ready web components** covering every aspect of server management:
|
`@serve.zone/catalog` provides **34 production-ready web components** covering every aspect of server management:
|
||||||
|
|
||||||
- 📊 **Dashboard** — Real-time cluster overview, resource usage, traffic metrics, quick actions
|
- 📊 **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
|
- 🐳 **Services** — Docker container management, deployment, logs, live stats, backups, and an integrated IDE workspace
|
||||||
|
- 🛒 **App Store** — Browse and deploy pre-configured application templates (WordPress, Gitea, etc.)
|
||||||
- 🌐 **Network** — Reverse proxy configuration, DNS record management, domain & SSL certificate monitoring
|
- 🌐 **Network** — Reverse proxy configuration, DNS record management, domain & SSL certificate monitoring
|
||||||
|
- 🔀 **Routes** — SmartProxy route configuration, match criteria, TLS modes, security profiles, forwarding targets
|
||||||
|
- 📧 **MTA / Email** — Inbound and outbound email management, SMTP transaction logs, authentication results
|
||||||
- 📦 **Registries** — Container registry management (onebox + external registries like Docker Hub, GHCR, ECR)
|
- 📦 **Registries** — Container registry management (onebox + external registries like Docker Hub, GHCR, ECR)
|
||||||
- 🔑 **Auth** — Login view, API token management (global + CI tokens)
|
- 🔑 **Auth** — Login view, API token management (global + CI tokens)
|
||||||
- ⚙️ **Settings** — Appearance, Cloudflare integration, SSL/TLS config, network settings, account management
|
- ⚙️ **Settings** — Appearance, Cloudflare integration, SSL/TLS config, network settings, account management
|
||||||
- 🏗️ **Platform Services** — MongoDB, MinIO, ClickHouse, Redis, Caddy monitoring and control
|
- 🏗️ **Platform Services** — MongoDB, MinIO, ClickHouse, Redis, Caddy monitoring and control
|
||||||
|
- 📋 **Configuration** — Read-only overview of the running server configuration with collapsible sections
|
||||||
|
|
||||||
Every component supports **light and dark themes** out of the box and communicates via standard `CustomEvent` dispatching.
|
Every component supports **light and dark themes** out of the box and communicates via standard `CustomEvent` dispatching.
|
||||||
|
|
||||||
@@ -85,6 +89,12 @@ import '@serve.zone/catalog';
|
|||||||
| `SzServiceCreateView` | `<sz-service-create-view>` | Service deployment form — image, ports, env vars, volumes, resource limits |
|
| `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 |
|
| `SzServicesBackupsView` | `<sz-services-backups-view>` | Backup schedule and backup history management |
|
||||||
|
|
||||||
|
### App Store
|
||||||
|
|
||||||
|
| Component | Tag | Description |
|
||||||
|
|-----------|-----|-------------|
|
||||||
|
| `SzAppStoreView` | `<sz-app-store-view>` | App marketplace for deploying pre-configured templates (WordPress, Gitea, etc.) with category filtering |
|
||||||
|
|
||||||
### Platform Services
|
### Platform Services
|
||||||
|
|
||||||
| Component | Tag | Description |
|
| Component | Tag | Description |
|
||||||
@@ -104,6 +114,20 @@ import '@serve.zone/catalog';
|
|||||||
| `SzDnsSslCard` | `<sz-dns-ssl-card>` | Cloudflare DNS and ACME config status |
|
| `SzDnsSslCard` | `<sz-dns-ssl-card>` | Cloudflare DNS and ACME config status |
|
||||||
| `SzCertificatesCard` | `<sz-certificates-card>` | Certificate status counts — valid, expiring, expired |
|
| `SzCertificatesCard` | `<sz-certificates-card>` | Certificate status counts — valid, expiring, expired |
|
||||||
|
|
||||||
|
### Routes
|
||||||
|
|
||||||
|
| Component | Tag | Description |
|
||||||
|
|-----------|-----|-------------|
|
||||||
|
| `SzRouteListView` | `<sz-route-list-view>` | Route configuration list with type filtering (HTTPS, email, DNS, etc.) |
|
||||||
|
| `SzRouteCard` | `<sz-route-card>` | Single route card — match criteria, action type, TLS mode, targets, security profile |
|
||||||
|
|
||||||
|
### MTA / Email
|
||||||
|
|
||||||
|
| Component | Tag | Description |
|
||||||
|
|-----------|-----|-------------|
|
||||||
|
| `SzMtaListView` | `<sz-mta-list-view>` | Email management — inbound/outbound messages with status badges and filtering |
|
||||||
|
| `SzMtaDetailView` | `<sz-mta-detail-view>` | Email detail — SMTP transaction log, TLS info, SPF/DKIM/DMARC results, headers, body |
|
||||||
|
|
||||||
### Registries
|
### Registries
|
||||||
|
|
||||||
| Component | Tag | Description |
|
| Component | Tag | Description |
|
||||||
@@ -119,6 +143,13 @@ import '@serve.zone/catalog';
|
|||||||
| `SzTokensView` | `<sz-tokens-view>` | API token management — global and CI tokens with copy/regenerate/delete |
|
| `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 |
|
| `SzSettingsView` | `<sz-settings-view>` | Full settings panel — appearance, Cloudflare, SSL/TLS, network, account |
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
| Component | Tag | Description |
|
||||||
|
|-----------|-----|-------------|
|
||||||
|
| `SzConfigOverview` | `<sz-config-overview>` | Top-level configuration overview with informational banner |
|
||||||
|
| `SzConfigSection` | `<sz-config-section>` | Collapsible config section — icon, enabled/disabled badge, key-value fields, action buttons |
|
||||||
|
|
||||||
## 🏗️ Architecture
|
## 🏗️ Architecture
|
||||||
|
|
||||||
### Component Pattern
|
### Component Pattern
|
||||||
@@ -186,8 +217,20 @@ import type { IServiceDetail, IServiceStats, ILogEntry, IServiceBackup } from '@
|
|||||||
// Network
|
// Network
|
||||||
import type { IDomainDetail, ICertificateDetail, IDnsRecord, ITrafficTarget } from '@serve.zone/catalog';
|
import type { IDomainDetail, ICertificateDetail, IDnsRecord, ITrafficTarget } from '@serve.zone/catalog';
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTls, IRouteSecurity } from '@serve.zone/catalog';
|
||||||
|
|
||||||
|
// MTA / Email
|
||||||
|
import type { IEmail, IEmailDetail, ISmtpLogEntry, IConnectionInfo, IAuthenticationResults } from '@serve.zone/catalog';
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
import type { IConfigField, IConfigSectionAction } from '@serve.zone/catalog';
|
||||||
|
|
||||||
// Settings & Auth
|
// Settings & Auth
|
||||||
import type { ISettings, IToken, IExternalRegistry } from '@serve.zone/catalog';
|
import type { ISettings, IToken, IExternalRegistry } from '@serve.zone/catalog';
|
||||||
|
|
||||||
|
// App Store
|
||||||
|
import type { IAppTemplate } from '@serve.zone/catalog';
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🛠️ Development
|
## 🛠️ Development
|
||||||
@@ -210,7 +253,7 @@ The **wcctools dev server** provides an interactive dashboard where every compon
|
|||||||
|
|
||||||
## 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.
|
||||||
|
|
||||||
@@ -222,7 +265,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
|
|||||||
|
|
||||||
### Company Information
|
### Company Information
|
||||||
|
|
||||||
Task Venture Capital GmbH
|
Task Venture Capital GmbH
|
||||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/catalog',
|
name: '@serve.zone/catalog',
|
||||||
version: '2.6.1',
|
version: '2.11.0',
|
||||||
description: 'UI component catalog for serve.zone'
|
description: 'UI component catalog for serve.zone'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,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';
|
||||||
|
|||||||
611
ts_web/elements/sz-app-store-view.ts
Normal file
611
ts_web/elements/sz-app-store-view.ts
Normal file
@@ -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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -587,17 +587,19 @@ export class SzPlatformServiceDetailView extends DeesElement {
|
|||||||
` : ''}
|
` : ''}
|
||||||
|
|
||||||
<!-- 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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,12 +55,28 @@ export interface IRouteSecurity {
|
|||||||
rateLimit?: { enabled: boolean; maxRequests: number; window: number };
|
rateLimit?: { enabled: boolean; maxRequests: number; window: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IRouteMetadata {
|
||||||
|
securityProfileRef?: string;
|
||||||
|
networkTargetRef?: string;
|
||||||
|
securityProfileName?: string;
|
||||||
|
networkTargetName?: string;
|
||||||
|
lastResolvedAt?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRouteVpn {
|
||||||
|
enabled: boolean;
|
||||||
|
mandatory?: boolean;
|
||||||
|
allowedServerDefinedClientTags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
vpn?: IRouteVpn;
|
||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
priority?: number;
|
priority?: number;
|
||||||
@@ -127,6 +143,15 @@ export class SzRouteCard extends DeesElement {
|
|||||||
rateLimit: { enabled: true, maxRequests: 100, window: 60 },
|
rateLimit: { enabled: true, maxRequests: 100, window: 60 },
|
||||||
maxConnections: 1000,
|
maxConnections: 1000,
|
||||||
},
|
},
|
||||||
|
vpn: {
|
||||||
|
enabled: true,
|
||||||
|
mandatory: true,
|
||||||
|
allowedServerDefinedClientTags: ['admin', 'devops'],
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
securityProfileName: 'STANDARD',
|
||||||
|
networkTargetName: 'LOSSLESS_INFRA',
|
||||||
|
},
|
||||||
} satisfies IRouteConfig}
|
} satisfies IRouteConfig}
|
||||||
></sz-route-card>
|
></sz-route-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,6 +162,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`
|
||||||
@@ -296,6 +324,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,6 +474,83 @@ 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-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid ${cssManager.bdTheme('#f4f4f5', '#1a1a1a')};
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
||||||
|
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||||
|
color: ${cssManager.bdTheme('#52525b', '#a1a1aa')};
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
|
||||||
|
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
||||||
|
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.edit:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#93c5fd', 'rgba(59, 130, 246, 0.5)')};
|
||||||
|
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.delete:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#fca5a5', 'rgba(239, 68, 68, 0.5)')};
|
||||||
|
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -624,8 +744,36 @@ export class SzRouteCard extends DeesElement {
|
|||||||
`
|
`
|
||||||
: ''}
|
: ''}
|
||||||
|
|
||||||
|
<!-- VPN Section -->
|
||||||
|
${this.renderVpn()}
|
||||||
|
|
||||||
|
<!-- Linked References Section -->
|
||||||
|
${this.renderLinked()}
|
||||||
|
|
||||||
<!-- Feature Icons Row -->
|
<!-- Feature Icons Row -->
|
||||||
${this.renderFeatures()}
|
${this.renderFeatures()}
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
${this.showActions ? html`
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="action-btn edit" @click=${(e: Event) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.dispatchEvent(new CustomEvent('route-edit', {
|
||||||
|
detail: this.route,
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}));
|
||||||
|
}}>Edit</button>
|
||||||
|
<button class="action-btn delete" @click=${(e: Event) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.dispatchEvent(new CustomEvent('route-delete', {
|
||||||
|
detail: this.route,
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}));
|
||||||
|
}}>Delete</button>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -638,12 +786,72 @@ export class SzRouteCard extends DeesElement {
|
|||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderVpn(): TemplateResult {
|
||||||
|
const vpn = this.route?.vpn;
|
||||||
|
if (!vpn?.enabled) 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 ${vpn.mandatory !== false ? 'mandatory' : 'optional'}">
|
||||||
|
${vpn.mandatory !== false ? 'VPN Only' : 'VPN + Public'}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
${vpn.allowedServerDefinedClientTags?.length ? html`
|
||||||
|
<div class="field-row">
|
||||||
|
<span class="field-key">Tags</span>
|
||||||
|
<span class="field-value">
|
||||||
|
${vpn.allowedServerDefinedClientTags.map(
|
||||||
|
(tag) => html`<span class="vpn-tag">${tag}</span>`
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderLinked(): TemplateResult {
|
||||||
|
const meta = this.route?.metadata;
|
||||||
|
if (!meta) return html``;
|
||||||
|
const hasProfile = !!meta.securityProfileName;
|
||||||
|
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.securityProfileName}</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 +868,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?.vpn?.enabled) {
|
||||||
|
features.push(html`<span class="feature"><span class="feature-icon">🔐</span>VPN</span>`);
|
||||||
|
}
|
||||||
|
if (meta?.securityProfileName || 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>
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -396,68 +396,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;
|
||||||
@@ -621,35 +559,17 @@ export class SzServiceDetailView extends DeesElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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">
|
||||||
|
|||||||
@@ -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