v2
This commit is contained in:
commit
787aab5e1d
16 changed files with 7129 additions and 0 deletions
7
.eslintrc.json
Normal file
7
.eslintrc.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals",
|
||||
"rules": {
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn"
|
||||
}
|
||||
}
|
||||
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
117
README.md
Normal file
117
README.md
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# Homelab Dashboard
|
||||
|
||||
Ein modernes, responsives Dashboard für die Verwaltung von Homelab-Diensten, gebaut mit **Next.js 15**, **TypeScript** und **Tailwind CSS**.
|
||||
|
||||
## Features
|
||||
|
||||
✨ **Moderne Benutzeroberfläche**
|
||||
- Elegantes Dark/Light Mode Design
|
||||
- Responsive Grid-Layout für alle Bildschirmgrößen
|
||||
- Smooth Animations und Übergänge
|
||||
|
||||
🎯 **Service-Management**
|
||||
- Service-Karten mit Name, Beschreibung und Status
|
||||
- Farbcodierter Status-Indikator (Online, Warnung, Offline)
|
||||
- Direkte Links zu Services
|
||||
- Pulsierender Status-Punkt für visuelle Rückmeldung
|
||||
|
||||
📱 **Mobile-First Design**
|
||||
- Optimiert für Smartphones, Tablets und Desktop
|
||||
- Touch-freundliche Buttons
|
||||
- Flexibles Grid-System
|
||||
|
||||
## Installation & Setup
|
||||
|
||||
### Voraussetzungen
|
||||
- Node.js 18+ installiert
|
||||
- npm oder yarn
|
||||
|
||||
### Schritt 1: Dependencies installieren
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Schritt 2: Entwicklungsserver starten
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Der Server läuft dann unter `http://localhost:3000`
|
||||
|
||||
## Projekt-Struktur
|
||||
|
||||
```
|
||||
homelab-dashboard/
|
||||
├── app/
|
||||
│ ├── components/
|
||||
│ │ ├── Header.tsx # Header-Komponente
|
||||
│ │ └── ServiceCard.tsx # Service-Karten-Komponente
|
||||
│ ├── globals.css # Globale Tailwind Styles
|
||||
│ ├── layout.tsx # Root Layout
|
||||
│ └── page.tsx # Startseite
|
||||
├── lib/
|
||||
│ └── data.ts # Mock-Daten und Service-Typen
|
||||
├── tailwind.config.ts # Tailwind Konfiguration
|
||||
├── postcss.config.js # PostCSS Konfiguration
|
||||
├── next.config.js # Next.js Konfiguration
|
||||
├── tsconfig.json # TypeScript Konfiguration
|
||||
└── package.json # Abhängigkeiten
|
||||
```
|
||||
|
||||
## Dienste hinzufügen/bearbeiten
|
||||
|
||||
Bearbeite die Datei [src/data/services.ts](src/data/services.ts):
|
||||
|
||||
```typescript
|
||||
export const services: Service[] = [
|
||||
{
|
||||
id: 'unique-id',
|
||||
name: 'Dein Service',
|
||||
description: 'Was macht dieser Service?',
|
||||
category: 'Infrastruktur', // oder Smart Home, Medien, Dokumente
|
||||
status: 'online', // online | warning | offline
|
||||
url: 'http://localhost:8000',
|
||||
icon: '🖥️', // Optional - ein Emoji für visuelles Feedback
|
||||
},
|
||||
// ... weitere Services
|
||||
];
|
||||
```
|
||||
|
||||
### Verfügbare Kategorien
|
||||
|
||||
- **Infrastruktur**: Server, Netzwerk, Monitoring
|
||||
- **Smart Home**: Home Automation und IoT
|
||||
- **Medien**: Medienserver und Downloading
|
||||
- **Dokumente**: Dateispeicher und Code-Repositories
|
||||
|
||||
## Verfügbare Commands
|
||||
|
||||
- `npm run dev` - Entwicklungsserver starten
|
||||
- `npm run build` - Produktions-Build erstellen
|
||||
- `npm start` - Produktions-Server starten
|
||||
- `npm run lint` - Linter ausführen
|
||||
|
||||
## Status-Indikatoren
|
||||
|
||||
| Status | Farbe | Beschreibung |
|
||||
|--------|-------|-------------|
|
||||
| Online | 🟢 Grün | Service läuft einwandfrei |
|
||||
| Warning | 🟡 Orange | Service hat Probleme, läuft aber |
|
||||
| Offline | 🔴 Rot | Service ist nicht erreichbar |
|
||||
|
||||
## Styling & Anpassungen
|
||||
|
||||
Das Projekt nutzt **Tailwind CSS** für Styling. Eigene Farben und Konfiguration können in [tailwind.config.ts](tailwind.config.ts) angepasst werden.
|
||||
|
||||
## Browser-Unterstützung
|
||||
|
||||
Das Dashboard funktioniert auf modernen Browsern:
|
||||
- Chrome/Edge 90+
|
||||
- Firefox 88+
|
||||
- Safari 14+
|
||||
|
||||
## Lizenz
|
||||
|
||||
MIT
|
||||
18
app/components/Header.tsx
Normal file
18
app/components/Header.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
'use client';
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header className="bg-gradient-to-r from-slate-900 to-slate-800 border-b border-slate-700 shadow-lg">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-3xl sm:text-4xl font-bold text-white tracking-tight">
|
||||
Homelab Dashboard
|
||||
</h1>
|
||||
<p className="text-slate-400 text-sm">
|
||||
Übersicht aller verwalteten Dienste
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
97
app/components/ServiceCard.tsx
Normal file
97
app/components/ServiceCard.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
'use client';
|
||||
|
||||
import { Service } from '@/lib/data';
|
||||
|
||||
interface ServiceCardProps {
|
||||
service: Service;
|
||||
}
|
||||
|
||||
const statusConfig = {
|
||||
online: {
|
||||
bgColor: 'bg-green-50 dark:bg-green-950',
|
||||
textColor: 'text-green-700 dark:text-green-300',
|
||||
dotColor: 'bg-green-500',
|
||||
label: 'Online',
|
||||
},
|
||||
warning: {
|
||||
bgColor: 'bg-amber-50 dark:bg-amber-950',
|
||||
textColor: 'text-amber-700 dark:text-amber-300',
|
||||
dotColor: 'bg-amber-500',
|
||||
label: 'Warnung',
|
||||
},
|
||||
offline: {
|
||||
bgColor: 'bg-red-50 dark:bg-red-950',
|
||||
textColor: 'text-red-700 dark:text-red-300',
|
||||
dotColor: 'bg-red-500',
|
||||
label: 'Offline',
|
||||
},
|
||||
};
|
||||
|
||||
export function ServiceCard({ service }: ServiceCardProps) {
|
||||
const config = statusConfig[service.status];
|
||||
|
||||
return (
|
||||
<a
|
||||
href={service.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group block"
|
||||
>
|
||||
<div className="h-full bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg shadow-sm hover:shadow-md transition-all duration-300 overflow-hidden hover:border-slate-300 dark:hover:border-slate-600">
|
||||
<div className="p-6 flex flex-col gap-4 h-full">
|
||||
{/* Header mit Name und Status */}
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 flex items-center gap-3">
|
||||
{service.icon && <span className="text-2xl">{service.icon}</span>}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
|
||||
{service.name}
|
||||
</h3>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
{service.category}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex items-center gap-2 px-3 py-1 rounded-full ${config.bgColor}`}>
|
||||
<span className={`w-2 h-2 rounded-full ${config.dotColor} animate-pulse`} />
|
||||
<span className={`text-xs font-medium ${config.textColor}`}>
|
||||
{config.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Beschreibung */}
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400 flex-grow">
|
||||
{service.description}
|
||||
</p>
|
||||
|
||||
{/* Button */}
|
||||
<div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.open(service.url, '_blank');
|
||||
}}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors duration-200 flex items-center justify-center gap-2 group/btn"
|
||||
>
|
||||
Öffnen
|
||||
<svg
|
||||
className="w-4 h-4 group-hover/btn:translate-x-1 transition-transform"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
21
app/globals.css
Normal file
21
app/globals.css
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
* {
|
||||
@apply border-slate-200;
|
||||
}
|
||||
|
||||
html {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-slate-50;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700;
|
||||
}
|
||||
}
|
||||
20
app/layout.tsx
Normal file
20
app/layout.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { Metadata as NextMetadata } from 'next';
|
||||
|
||||
export const metadata: NextMetadata = {
|
||||
manifest: '/manifest.json',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="de">
|
||||
<body className="antialiased">
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
52
app/page.tsx
Normal file
52
app/page.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { Header } from './components/Header';
|
||||
import { ServiceCard } from './components/ServiceCard';
|
||||
import { services } from '@/lib/data';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Homelab Dashboard',
|
||||
description: 'Verwaltungs-Dashboard für dein Homelab',
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
|
||||
<Header />
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
{/* Services Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{services.map((service) => (
|
||||
<ServiceCard key={service.id} service={service} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Empty State (falls keine Services) */}
|
||||
{services.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<svg
|
||||
className="w-16 h-16 text-slate-400 mb-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
|
||||
/>
|
||||
</svg>
|
||||
<h3 className="text-xl font-semibold text-slate-900 dark:text-white">
|
||||
Keine Dienste konfiguriert
|
||||
</h3>
|
||||
<p className="text-slate-600 dark:text-slate-400 mt-2">
|
||||
Füge Dienste in der Datenkonfiguration hinzu, um sie hier anzuzeigen
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
2
lib/data.ts
Normal file
2
lib/data.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// Re-export from the main services data file
|
||||
export * from '@/src/data/services';
|
||||
4
next.config.js
Normal file
4
next.config.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {}
|
||||
|
||||
module.exports = nextConfig
|
||||
6521
package-lock.json
generated
Normal file
6521
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
29
package.json
Normal file
29
package.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "homelab-dashboard",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^16.2.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/react": "^18.2.46",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@typescript-eslint/eslint-plugin": "^8.58.2",
|
||||
"@typescript-eslint/parser": "^8.58.2",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^15.0.3",
|
||||
"postcss": "^8.4.32",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
10
postcss.config.js
Normal file
10
postcss.config.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import type { Config } from 'postcss-load-config'
|
||||
|
||||
const config: Config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
130
src/data/services.ts
Normal file
130
src/data/services.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
export type ServiceCategory = 'Infrastruktur' | 'Smart Home' | 'Medien' | 'Dokumente';
|
||||
export type ServiceStatus = 'online' | 'warning' | 'offline';
|
||||
|
||||
export interface Service {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: ServiceCategory;
|
||||
status: ServiceStatus;
|
||||
url: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export const services: Service[] = [
|
||||
// Infrastruktur
|
||||
{
|
||||
id: 'unraid',
|
||||
name: 'Unraid',
|
||||
description: 'NAS und Server Management Platform',
|
||||
category: 'Infrastruktur',
|
||||
status: 'online',
|
||||
url: 'http://localhost',
|
||||
icon: '🖥️',
|
||||
},
|
||||
{
|
||||
id: 'unifi',
|
||||
name: 'UniFi',
|
||||
description: 'Netzwerk-Management und WLAN-Controller',
|
||||
category: 'Infrastruktur',
|
||||
status: 'online',
|
||||
url: 'http://localhost:8443',
|
||||
icon: '🌐',
|
||||
},
|
||||
{
|
||||
id: 'checkmk',
|
||||
name: 'Checkmk',
|
||||
description: 'Monitoring und Alerting System',
|
||||
category: 'Infrastruktur',
|
||||
status: 'online',
|
||||
url: 'http://localhost/checkmk',
|
||||
icon: '📊',
|
||||
},
|
||||
{
|
||||
id: 'nginx-proxy',
|
||||
name: 'Nginx Proxy Manager',
|
||||
description: 'Reverse Proxy und SSL-Verwaltung',
|
||||
category: 'Infrastruktur',
|
||||
status: 'online',
|
||||
url: 'http://localhost:81',
|
||||
icon: '⚙️',
|
||||
},
|
||||
|
||||
// Smart Home
|
||||
{
|
||||
id: 'home-assistant',
|
||||
name: 'Home Assistant',
|
||||
description: 'Smart Home Automation und Integration',
|
||||
category: 'Smart Home',
|
||||
status: 'offline',
|
||||
url: 'http://localhost:8123',
|
||||
icon: '🏠',
|
||||
},
|
||||
|
||||
// Medien
|
||||
{
|
||||
id: 'plex',
|
||||
name: 'Plex',
|
||||
description: 'Medienserver und Streaming-Dienst',
|
||||
category: 'Medien',
|
||||
status: 'online',
|
||||
url: 'http://localhost:32400',
|
||||
icon: '🎬',
|
||||
},
|
||||
{
|
||||
id: 'jellyfin',
|
||||
name: 'Jellyfin',
|
||||
description: 'Open-Source Medienserver',
|
||||
category: 'Medien',
|
||||
status: 'online',
|
||||
url: 'http://localhost:8096',
|
||||
icon: '📺',
|
||||
},
|
||||
{
|
||||
id: 'radarr',
|
||||
name: 'Radarr',
|
||||
description: 'Automatisches Filmmanagement',
|
||||
category: 'Medien',
|
||||
status: 'online',
|
||||
url: 'http://localhost:7878',
|
||||
icon: '🎥',
|
||||
},
|
||||
{
|
||||
id: 'sonarr',
|
||||
name: 'Sonarr',
|
||||
description: 'Automatisches Serienmanagement',
|
||||
category: 'Medien',
|
||||
status: 'online',
|
||||
url: 'http://localhost:8989',
|
||||
icon: '📺',
|
||||
},
|
||||
{
|
||||
id: 'prowlarr',
|
||||
name: 'Prowlarr',
|
||||
description: 'Indexer und Proxy Manager',
|
||||
category: 'Medien',
|
||||
status: 'warning',
|
||||
url: 'http://localhost:9696',
|
||||
icon: '🔍',
|
||||
},
|
||||
|
||||
// Dokumente
|
||||
{
|
||||
id: 'seafile',
|
||||
name: 'Seafile',
|
||||
description: 'Dateifreigabe und Cloud Storage',
|
||||
category: 'Dokumente',
|
||||
status: 'online',
|
||||
url: 'http://localhost:8000',
|
||||
icon: '☁️',
|
||||
},
|
||||
{
|
||||
id: 'forgejo',
|
||||
name: 'Forgejo',
|
||||
description: 'Self-hosted Git und Code Repository',
|
||||
category: 'Dokumente',
|
||||
status: 'online',
|
||||
url: 'http://localhost:3000',
|
||||
icon: '💾',
|
||||
},
|
||||
];
|
||||
18
tailwind.config.ts
Normal file
18
tailwind.config.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import type { Config } from 'tailwindcss'
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
success: '#10b981',
|
||||
warning: '#f59e0b',
|
||||
error: '#ef4444',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
export default config
|
||||
49
tsconfig.json
Normal file
49
tsconfig.json
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowJs": true,
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue