mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-18 20:40:59 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c52267bbb6 | ||
|
|
e93556e537 | ||
|
|
391cfbc50c | ||
|
|
608da9ce1c | ||
|
|
b35174f857 | ||
|
|
ae18cd1a12 | ||
|
|
68dd9fc173 | ||
|
|
f95720758e | ||
|
|
d4c07fdc8e | ||
|
|
28fa65df9d | ||
|
|
c52edb75df | ||
|
|
11fe75dd57 | ||
|
|
ec107ca0ce | ||
|
|
7172d4da90 | ||
|
|
badcbfdb17 | ||
|
|
748d5c0939 | ||
|
|
dd7426a01f | ||
|
|
fde60b7d68 | ||
|
|
297e5ca92b | ||
|
|
405b2e605e | ||
|
|
1f94641dee | ||
|
|
9249b84ddd | ||
|
|
a7cd021ff2 | ||
|
|
8424e97cf8 | ||
|
|
f128eff8de | ||
|
|
db1a0ad5f5 | ||
|
|
8e1321d251 | ||
|
|
ef0829aaa4 | ||
|
|
4d393e17ab | ||
|
|
d35f7bfe53 | ||
|
|
b91b0e8796 | ||
|
|
233f1779d8 | ||
|
|
0cebfcf1f5 | ||
|
|
fb3f834c36 | ||
|
|
30d0725331 | ||
|
|
3140d9e143 | ||
|
|
db6259f8a3 | ||
|
|
49a586db53 | ||
|
|
aa2149e2cf |
@@ -29,6 +29,23 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Needed to get all tags for versioning
|
fetch-depth: 0 # Needed to get all tags for versioning
|
||||||
|
|
||||||
|
# Set up Node.js for frontend build
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
# Build frontend assets
|
||||||
|
- name: Build frontend assets
|
||||||
|
run: |
|
||||||
|
node build.js
|
||||||
|
ls -la static/dist/
|
||||||
|
|
||||||
# Set version information
|
# Set version information
|
||||||
- name: Set Version
|
- name: Set Version
|
||||||
id: version
|
id: version
|
||||||
|
|||||||
@@ -30,6 +30,23 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Needed to get all tags for versioning
|
fetch-depth: 0 # Needed to get all tags for versioning
|
||||||
|
|
||||||
|
# Set up Node.js for frontend build
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
# Build frontend assets
|
||||||
|
- name: Build frontend assets
|
||||||
|
run: |
|
||||||
|
node build.js
|
||||||
|
ls -la static/dist/
|
||||||
|
|
||||||
# Set version information
|
# Set version information
|
||||||
- name: Set Version
|
- name: Set Version
|
||||||
id: version
|
id: version
|
||||||
|
|||||||
@@ -27,6 +27,26 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install Node.js dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build frontend assets
|
||||||
|
run: |
|
||||||
|
# Build JavaScript and CSS assets
|
||||||
|
node build.js
|
||||||
|
|
||||||
|
# Ensure the dist directory exists
|
||||||
|
mkdir -p static/dist
|
||||||
|
|
||||||
|
# Verify the build output
|
||||||
|
ls -la static/dist
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
|
|||||||
+4
-15
@@ -43,20 +43,7 @@ node_modules/
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Ignore all Go files in the components directory
|
# Ignore all Go files in the components directory
|
||||||
components/*.go
|
*_templ.go
|
||||||
!components/components.go
|
|
||||||
|
|
||||||
components/providers/*.go
|
|
||||||
!components/providers/providers.go
|
|
||||||
|
|
||||||
components/providers/source/*.go
|
|
||||||
!components/providers/source/source.go
|
|
||||||
|
|
||||||
components/providers/destination/*.go
|
|
||||||
!components/providers/destination/destination.go
|
|
||||||
|
|
||||||
components/providers/common/*.go
|
|
||||||
!components/providers/common/common.go
|
|
||||||
|
|
||||||
# Ignore the data directory
|
# Ignore the data directory
|
||||||
data/
|
data/
|
||||||
@@ -79,6 +66,8 @@ backups/
|
|||||||
/destination/
|
/destination/
|
||||||
/archive/
|
/archive/
|
||||||
|
|
||||||
|
# Ignore the dist directory
|
||||||
|
static/dist/
|
||||||
|
|
||||||
# Ignore binaries
|
# Ignore binaries
|
||||||
gomft
|
gomft
|
||||||
+44
-3
@@ -1,3 +1,32 @@
|
|||||||
|
# Build frontend assets
|
||||||
|
FROM node:20-alpine AS frontend-builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy all files needed for the build first
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
COPY build.js ./
|
||||||
|
COPY static/ ./static/
|
||||||
|
COPY tailwind.config.js ./
|
||||||
|
|
||||||
|
# Debug: Show the contents of build.js
|
||||||
|
RUN echo "Contents of build.js:" && cat build.js
|
||||||
|
|
||||||
|
# Remove the build script from postinstall
|
||||||
|
RUN sed -i 's/"postinstall": "npm run build",//' package.json
|
||||||
|
|
||||||
|
# Install dependencies and build with verbose output
|
||||||
|
RUN npm ci && \
|
||||||
|
echo "Building frontend assets..." && \
|
||||||
|
node build.js && \
|
||||||
|
echo "Build complete. Contents of dist:" && \
|
||||||
|
ls -la static/dist/ && \
|
||||||
|
echo "Sample of app.js:" && \
|
||||||
|
head -n 10 static/dist/app.js && \
|
||||||
|
echo "Sample of app.css:" && \
|
||||||
|
head -n 10 static/dist/app.css
|
||||||
|
|
||||||
|
# Go build stage
|
||||||
FROM golang:1.24-alpine AS builder
|
FROM golang:1.24-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -17,7 +46,20 @@ RUN go install github.com/a-h/templ/cmd/templ@latest
|
|||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
# Copy the rest of the source code
|
# Create static directory structure
|
||||||
|
RUN mkdir -p /app/static/dist
|
||||||
|
|
||||||
|
# Copy built frontend assets from frontend-builder BEFORE copying Go source
|
||||||
|
COPY --from=frontend-builder /app/static/dist/ /app/static/dist/
|
||||||
|
|
||||||
|
# Copy the rest of the static files
|
||||||
|
COPY static/ /app/static/
|
||||||
|
|
||||||
|
# Verify static files are in place before Go build
|
||||||
|
RUN echo "Verifying static files before Go build:" && \
|
||||||
|
ls -la /app/static/dist/
|
||||||
|
|
||||||
|
# Now copy the rest of the source code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Generate template files from .templ files
|
# Generate template files from .templ files
|
||||||
@@ -61,8 +103,7 @@ RUN addgroup -g ${GID} ${USERNAME} && \
|
|||||||
COPY --from=builder /app/gomft /app/
|
COPY --from=builder /app/gomft /app/
|
||||||
COPY --from=builder /usr/local/bin/rclone /usr/local/bin/rclone
|
COPY --from=builder /usr/local/bin/rclone /usr/local/bin/rclone
|
||||||
|
|
||||||
# Copy static files and configurations
|
# Copy components
|
||||||
COPY static/ /app/static/
|
|
||||||
COPY components/ /app/components/
|
COPY components/ /app/components/
|
||||||
|
|
||||||
# Copy entrypoint script
|
# Copy entrypoint script
|
||||||
|
|||||||
@@ -52,6 +52,15 @@ GoMFT is a web-based managed file transfer application built with Go, leveraging
|
|||||||
- Custom HTTP headers
|
- Custom HTTP headers
|
||||||
- Selectable events (job success, job failure)
|
- Selectable events (job success, job failure)
|
||||||
- Detailed JSON payload with job information
|
- Detailed JSON payload with job information
|
||||||
|
- **Multiple Notification Services**: Get job status updates through various notification channels:
|
||||||
|
- Email notifications with configurable SMTP settings
|
||||||
|
- Webhooks with authentication for custom integrations
|
||||||
|
- Pushbullet notifications with optional device targeting
|
||||||
|
- Ntfy.sh (both public and self-hosted) for simple push notifications
|
||||||
|
- Gotify server integration for self-hosted notifications
|
||||||
|
- Pushover notifications with customizable sounds and priorities
|
||||||
|
- Configurable message templates for all notification types
|
||||||
|
- Event-based triggers (job start, completion, errors)
|
||||||
- **Scheduled Transfers**: Configure transfers using cron expressions with flexible scheduling options
|
- **Scheduled Transfers**: Configure transfers using cron expressions with flexible scheduling options
|
||||||
- **Transfer Monitoring**: Real-time status updates and detailed transfer logs with bytes and files transferred statistics
|
- **Transfer Monitoring**: Real-time status updates and detailed transfer logs with bytes and files transferred statistics
|
||||||
- **File Metadata Tracking**: Complete history and status of all transferred files with detailed information:
|
- **File Metadata Tracking**: Complete history and status of all transferred files with detailed information:
|
||||||
@@ -77,6 +86,14 @@ GoMFT is a web-based managed file transfer application built with Go, leveraging
|
|||||||
- **Transfer Configurations**: Full control over source and destination connection parameters
|
- **Transfer Configurations**: Full control over source and destination connection parameters
|
||||||
- **Job Management**: Create, edit, and monitor transfer jobs with scheduling
|
- **Job Management**: Create, edit, and monitor transfer jobs with scheduling
|
||||||
- **Security**: Role-based access control with admin-managed user accounts and secure password management
|
- **Security**: Role-based access control with admin-managed user accounts and secure password management
|
||||||
|
- **Authentication Providers**: Flexible authentication options:
|
||||||
|
- Built-in email/password authentication
|
||||||
|
- Authentik integration for enterprise SSO
|
||||||
|
- OpenID Connect (OIDC) support for standard identity providers
|
||||||
|
- OAuth2 integration for popular providers (Google, GitHub, etc.)
|
||||||
|
- Multiple provider support with fallback options
|
||||||
|
- Automatic user provisioning from external providers
|
||||||
|
- Role mapping from external identity providers
|
||||||
- **Password Recovery**: Self-service password reset via email with secure token-based authentication
|
- **Password Recovery**: Self-service password reset via email with secure token-based authentication
|
||||||
- **User Profile Management**: Personal settings including theme preferences
|
- **User Profile Management**: Personal settings including theme preferences
|
||||||
- **Modern UI**: Built with Templ, HTMX and Tailwind CSS for a responsive experience
|
- **Modern UI**: Built with Templ, HTMX and Tailwind CSS for a responsive experience
|
||||||
@@ -294,6 +311,10 @@ PGID=1000
|
|||||||
- If longer than 32 bytes, it will be truncated to 32 bytes
|
- If longer than 32 bytes, it will be truncated to 32 bytes
|
||||||
- Example: `TOTP_ENCRYPTION_KEY=abcdefghijklmnopqrstuvwxyz123456`
|
- Example: `TOTP_ENCRYPTION_KEY=abcdefghijklmnopqrstuvwxyz123456`
|
||||||
|
|
||||||
|
|
||||||
|
- SSL/TLS Verification Control:
|
||||||
|
- `SKIP_SSL_VERIFY`: Set to `true` to disable SSL/TLS certificate verification for outgoing connections (e.g., webhooks, email). Use with caution, as this can expose connections to man-in-the-middle attacks. Defaults to `false` (verification enabled).
|
||||||
|
- Example: `SKIP_SSL_VERIFY=true`
|
||||||
### Logging Configuration
|
### Logging Configuration
|
||||||
|
|
||||||
GoMFT provides configurable logging with rotation support through the following environment variables:
|
GoMFT provides configurable logging with rotation support through the following environment variables:
|
||||||
@@ -355,7 +376,41 @@ Log files contain detailed information about file transfers, job execution, and
|
|||||||
- Choose notification triggers (job success, job failure, or both)
|
- Choose notification triggers (job success, job failure, or both)
|
||||||
- Test your webhook integration with manual job runs
|
- Test your webhook integration with manual job runs
|
||||||
|
|
||||||
8. Manage file metadata:
|
8. **Webhook Notifications**:
|
||||||
|
- **Webhook Integration**: Send notifications to external systems when jobs complete
|
||||||
|
- **Secure Authentication**: HMAC-SHA256 signature for webhook verification
|
||||||
|
- **Custom Headers**: Add custom HTTP headers to webhook requests
|
||||||
|
- **Flexible Configuration**: Configure different webhooks for different jobs
|
||||||
|
- **Event Selection**: Choose to send notifications on success, failure, or both
|
||||||
|
- **Detailed Payload**: Rich JSON payload with complete job execution details
|
||||||
|
|
||||||
|
9. **Multiple Notification Services**:
|
||||||
|
- **Pushbullet Integration**: Send notifications to your devices through Pushbullet
|
||||||
|
- Device targeting support for specific device delivery
|
||||||
|
- Customizable title and message templates
|
||||||
|
- API key-based authentication
|
||||||
|
- **Ntfy Integration**: Use public ntfy.sh or self-hosted ntfy server
|
||||||
|
- Topic-based routing of notifications
|
||||||
|
- Priority levels for different job events
|
||||||
|
- Optional username/password authentication for private servers
|
||||||
|
- Customizable title and message templates
|
||||||
|
- **Gotify Integration**: Send notifications to self-hosted Gotify servers
|
||||||
|
- Application token-based authentication
|
||||||
|
- Priority levels (1-10) for different notification importance
|
||||||
|
- Customizable title and message templates
|
||||||
|
- **Pushover Integration**: Professional notification delivery service
|
||||||
|
- Application and user key authentication
|
||||||
|
- Device targeting for selective delivery
|
||||||
|
- Sound selection for different notification types
|
||||||
|
- Priority levels from lowest to emergency
|
||||||
|
- Customizable title and message templates
|
||||||
|
- **Common Features**:
|
||||||
|
- Variable substitution in notification templates
|
||||||
|
- Job data access in templates (status, files, bytes, times)
|
||||||
|
- Event-based filtering (job start, completion, errors)
|
||||||
|
- Success/failure tracking for diagnostic purposes
|
||||||
|
|
||||||
|
10. Manage file metadata:
|
||||||
- Navigate to the "Files" section to view all processed files
|
- Navigate to the "Files" section to view all processed files
|
||||||
- Use filters to quickly find files by status, job ID, or filename
|
- Use filters to quickly find files by status, job ID, or filename
|
||||||
- Click on any file to view detailed metadata including timestamps, size, and hash
|
- Click on any file to view detailed metadata including timestamps, size, and hash
|
||||||
@@ -363,7 +418,7 @@ Log files contain detailed information about file transfers, job execution, and
|
|||||||
- Delete file metadata records when no longer needed
|
- Delete file metadata records when no longer needed
|
||||||
- View files associated with specific jobs by navigating from the job details
|
- View files associated with specific jobs by navigating from the job details
|
||||||
|
|
||||||
9. Utilize admin tools (administrators only):
|
11. Utilize admin tools (administrators only):
|
||||||
- Access the "Admin Tools" section from the navigation menu
|
- Access the "Admin Tools" section from the navigation menu
|
||||||
- View system statistics and server information
|
- View system statistics and server information
|
||||||
- Create and manage database backups
|
- Create and manage database backups
|
||||||
@@ -373,19 +428,57 @@ Log files contain detailed information about file transfers, job execution, and
|
|||||||
|
|
||||||
### User Management
|
### User Management
|
||||||
|
|
||||||
GoMFT uses a role-based access control system:
|
GoMFT uses a role-based access control system with flexible authentication options:
|
||||||
|
|
||||||
- **Administrators**: Can create and manage users, access all features
|
- **Administrators**: Can create and manage users, access all features
|
||||||
- **Regular Users**: Can manage transfers and view history
|
- **Regular Users**: Can manage transfers and view history
|
||||||
|
|
||||||
User management features:
|
#### Authentication Options
|
||||||
- Only administrators can create new user accounts
|
|
||||||
- User passwords are securely hashed with bcrypt
|
1. **Built-in Authentication**:
|
||||||
- Password history tracking prevents reuse of recent passwords
|
- Email/password login with secure password hashing
|
||||||
- Account lockout after multiple failed login attempts
|
- JWT-based session management
|
||||||
- Self-service password reset via secure email links
|
- Password history tracking
|
||||||
- JWT-based authentication with tokens
|
- Account lockout protection
|
||||||
- User theme preference settings (light/dark)
|
- Self-service password reset
|
||||||
|
|
||||||
|
2. **External Authentication Providers**:
|
||||||
|
- **Authentik Integration**:
|
||||||
|
- Enterprise-grade SSO capabilities
|
||||||
|
- Automatic user provisioning
|
||||||
|
- Role synchronization
|
||||||
|
- Group mapping support
|
||||||
|
- Secure token exchange
|
||||||
|
|
||||||
|
- **OpenID Connect (OIDC)**:
|
||||||
|
- Standard-compliant identity provider support
|
||||||
|
- Automatic user creation and updates
|
||||||
|
- Role mapping from OIDC claims
|
||||||
|
- Multiple provider support
|
||||||
|
- Secure token validation
|
||||||
|
|
||||||
|
- **OAuth2 Providers**:
|
||||||
|
- Google authentication
|
||||||
|
- GitHub integration
|
||||||
|
- Other OAuth2-compliant providers
|
||||||
|
- Custom provider configuration
|
||||||
|
- Automatic profile synchronization
|
||||||
|
|
||||||
|
3. **Security Features**:
|
||||||
|
- Secure password hashing with bcrypt
|
||||||
|
- JWT-based authentication with tokens
|
||||||
|
- Password history tracking prevents reuse
|
||||||
|
- Account lockout after failed attempts
|
||||||
|
- Two-factor authentication support
|
||||||
|
- Session management and timeout
|
||||||
|
- Secure token storage and handling
|
||||||
|
|
||||||
|
4. **User Profile Management**:
|
||||||
|
- Theme preferences (light/dark mode)
|
||||||
|
- Profile information updates
|
||||||
|
- Password change functionality
|
||||||
|
- Two-factor authentication setup
|
||||||
|
- External account linking
|
||||||
|
|
||||||
### Two-Factor Authentication (2FA) Implementation
|
### Two-Factor Authentication (2FA) Implementation
|
||||||
|
|
||||||
@@ -479,13 +572,16 @@ The following fields have been added to the `users` table:
|
|||||||
- Manual execution
|
- Manual execution
|
||||||
- Enable/disable schedules
|
- Enable/disable schedules
|
||||||
|
|
||||||
8. **Webhook Notifications**:
|
8. **Notification Options**:
|
||||||
- **Webhook Integration**: Send notifications to external systems when jobs complete
|
- **Email Notifications**: Receive job status updates via email
|
||||||
- **Secure Authentication**: HMAC-SHA256 signature for webhook verification
|
- **Webhook Notifications**: Integration with external systems
|
||||||
- **Custom Headers**: Add custom HTTP headers to webhook requests
|
- **Pushbullet**: Push notifications to your devices
|
||||||
- **Flexible Configuration**: Configure different webhooks for different jobs
|
- **Ntfy**: Simple push notifications via ntfy.sh
|
||||||
- **Event Selection**: Choose to send notifications on success, failure, or both
|
- **Gotify**: Self-hosted notification server integration
|
||||||
- **Detailed Payload**: Rich JSON payload with complete job execution details
|
- **Pushover**: Professional notification service
|
||||||
|
- Configure event triggers (start, complete, error)
|
||||||
|
- Customize notification message templates
|
||||||
|
- Selective notification based on job status
|
||||||
|
|
||||||
### Email Notifications
|
### Email Notifications
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import * as esbuild from 'esbuild';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const isWatch = process.argv.includes('--watch');
|
||||||
|
|
||||||
|
// Ensure the dist directory exists
|
||||||
|
const distDir = path.join(__dirname, 'static', 'dist');
|
||||||
|
if (!fs.existsSync(distDir)) {
|
||||||
|
fs.mkdirSync(distDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy Font Awesome files
|
||||||
|
const fontAwesomeSrcDir = path.join(__dirname, 'node_modules', '@fortawesome', 'fontawesome-free');
|
||||||
|
const fontAwesomeDestDir = path.join(distDir, 'fontawesome');
|
||||||
|
|
||||||
|
// Copy CSS files
|
||||||
|
const cssFiles = [
|
||||||
|
'css/all.min.css',
|
||||||
|
'css/fontawesome.min.css',
|
||||||
|
'css/solid.min.css',
|
||||||
|
'css/regular.min.css',
|
||||||
|
'css/brands.min.css'
|
||||||
|
];
|
||||||
|
|
||||||
|
cssFiles.forEach(file => {
|
||||||
|
const srcFile = path.join(fontAwesomeSrcDir, file);
|
||||||
|
const destFile = path.join(fontAwesomeDestDir, file);
|
||||||
|
const destDir = path.dirname(destFile);
|
||||||
|
|
||||||
|
if (!fs.existsSync(destDir)) {
|
||||||
|
fs.mkdirSync(destDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(srcFile)) {
|
||||||
|
fs.copyFileSync(srcFile, destFile);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy webfonts
|
||||||
|
const webfontsSrcDir = path.join(fontAwesomeSrcDir, 'webfonts');
|
||||||
|
const webfontsDestDir = path.join(fontAwesomeDestDir, 'webfonts');
|
||||||
|
|
||||||
|
if (!fs.existsSync(webfontsDestDir)) {
|
||||||
|
fs.mkdirSync(webfontsDestDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.readdirSync(webfontsSrcDir).forEach(file => {
|
||||||
|
fs.copyFileSync(
|
||||||
|
path.join(webfontsSrcDir, file),
|
||||||
|
path.join(webfontsDestDir, file)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const commonConfig = {
|
||||||
|
sourcemap: true,
|
||||||
|
minify: true,
|
||||||
|
bundle: true,
|
||||||
|
platform: 'browser',
|
||||||
|
target: ['es2020'],
|
||||||
|
};
|
||||||
|
|
||||||
|
async function buildTailwind() {
|
||||||
|
console.log('Building Tailwind CSS...');
|
||||||
|
execSync('npx tailwindcss -i ./static/css/app.css -o ./static/dist/app.css --minify');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function build() {
|
||||||
|
try {
|
||||||
|
// Build vendor JavaScript bundle (CDN dependencies)
|
||||||
|
await esbuild.build({
|
||||||
|
...commonConfig,
|
||||||
|
entryPoints: ['static/js/vendor.js'],
|
||||||
|
outfile: 'static/dist/vendor.js',
|
||||||
|
format: 'iife',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build application JavaScript
|
||||||
|
await esbuild.build({
|
||||||
|
...commonConfig,
|
||||||
|
entryPoints: ['static/js/app.js'],
|
||||||
|
outfile: 'static/dist/app.js',
|
||||||
|
format: 'iife',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build initialization JavaScript
|
||||||
|
await esbuild.build({
|
||||||
|
...commonConfig,
|
||||||
|
entryPoints: ['static/js/init.js'],
|
||||||
|
outfile: 'static/dist/init.js',
|
||||||
|
format: 'iife',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build CSS with Tailwind
|
||||||
|
await buildTailwind();
|
||||||
|
|
||||||
|
console.log('Build completed successfully!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Build failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWatch) {
|
||||||
|
// Watch mode
|
||||||
|
console.log('Starting watch mode...');
|
||||||
|
const ctx = await esbuild.context(commonConfig);
|
||||||
|
await ctx.watch();
|
||||||
|
|
||||||
|
// Watch Tailwind CSS
|
||||||
|
execSync('npx tailwindcss -i ./static/css/app.css -o ./static/dist/app.css --watch');
|
||||||
|
|
||||||
|
console.log('Watching for changes...');
|
||||||
|
} else {
|
||||||
|
// Single build
|
||||||
|
build();
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminLayout renders the main admin dashboard
|
||||||
|
templ AdminLayout(ctx context.Context) {
|
||||||
|
@LayoutWithContext("Administration", ctx) {
|
||||||
|
<div class="flex flex-col md:flex-row bg-gray-50 dark:bg-gray-900 min-h-screen">
|
||||||
|
<!-- Sidebar Navigation -->
|
||||||
|
<nav class="md:w-64 md:min-h-screen bg-white border-r border-gray-200 dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<div class="p-4 overflow-y-auto">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Admin Console</h2>
|
||||||
|
<ul class="space-y-2">
|
||||||
|
<li>
|
||||||
|
<a href="/admin" class="flex items-center p-2 text-gray-900 rounded-lg dark:text-white bg-gray-100 dark:bg-gray-700 group">
|
||||||
|
<i class="fas fa-tachometer-alt w-5 h-5 text-gray-500 dark:text-gray-400 mr-3"></i>
|
||||||
|
Dashboard
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/admin/users" class="flex items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group">
|
||||||
|
<i class="fas fa-users w-5 h-5 text-gray-500 dark:text-gray-400 mr-3"></i>
|
||||||
|
User Management
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/admin/roles" class="flex items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group">
|
||||||
|
<i class="fas fa-user-shield w-5 h-5 text-gray-500 dark:text-gray-400 mr-3"></i>
|
||||||
|
Role Management
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/admin/audit" class="flex items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group">
|
||||||
|
<i class="fas fa-clipboard-list w-5 h-5 text-gray-500 dark:text-gray-400 mr-3"></i>
|
||||||
|
Audit Logs
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/admin/database" class="flex items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group">
|
||||||
|
<i class="fas fa-database w-5 h-5 text-gray-500 dark:text-gray-400 mr-3"></i>
|
||||||
|
Database Tools
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/admin/settings" class="flex items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group">
|
||||||
|
<i class="fas fa-cog w-5 h-5 text-gray-500 dark:text-gray-400 mr-3"></i>
|
||||||
|
System Settings
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="flex-1 p-8">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||||
|
<!-- Statistics Cards -->
|
||||||
|
<div class="bg-white p-6 rounded-lg shadow-sm dark:bg-gray-800">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">Total Users</p>
|
||||||
|
<p class="text-2xl font-bold text-gray-900 dark:text-white">1,234</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-blue-100 p-3 rounded-full dark:bg-blue-900">
|
||||||
|
<i class="fas fa-users text-blue-500 w-6 h-6 dark:text-blue-300"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-6 rounded-lg shadow-sm dark:bg-gray-800">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">Active Sessions</p>
|
||||||
|
<p class="text-2xl font-bold text-gray-900 dark:text-white">42</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-green-100 p-3 rounded-full dark:bg-green-900">
|
||||||
|
<i class="fas fa-signal text-green-500 w-6 h-6 dark:text-green-300"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-6 rounded-lg shadow-sm dark:bg-gray-800">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">System Health</p>
|
||||||
|
<p class="text-2xl font-bold text-green-500 dark:text-green-400">98%</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-green-100 p-3 rounded-full dark:bg-green-900">
|
||||||
|
<i class="fas fa-heartbeat text-green-500 w-6 h-6 dark:text-green-300"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Activity Table -->
|
||||||
|
<div class="bg-white rounded-lg shadow-sm dark:bg-gray-800">
|
||||||
|
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Recent Activity</h3>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">User</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Action</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Timestamp</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<!-- Example Rows -->
|
||||||
|
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-900 dark:text-white">admin@example.com</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Updated user permissions</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">2024-02-20 14:32:10</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuditLogEntry represents an audit log entry for display
|
||||||
|
type AuditLogEntry struct {
|
||||||
|
ID uint
|
||||||
|
Action string
|
||||||
|
EntityType string
|
||||||
|
EntityID uint
|
||||||
|
Username string
|
||||||
|
UserID uint
|
||||||
|
Timestamp time.Time
|
||||||
|
Details map[string]interface{}
|
||||||
|
DetailsSummary string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuditLogsData struct {
|
||||||
|
Logs []AuditLogEntry
|
||||||
|
TotalPages int
|
||||||
|
CurrentPage int
|
||||||
|
TotalRecords int
|
||||||
|
FilterAction string
|
||||||
|
FilterEntity string
|
||||||
|
FilterUser string
|
||||||
|
FilterDateFrom string
|
||||||
|
FilterDateTo string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminAuditLogs renders the audit logs page
|
||||||
|
templ AdminAuditLogs(ctx context.Context, data AuditLogsData) {
|
||||||
|
@LayoutWithContext("Audit Logs", ctx) {
|
||||||
|
<div id="audit-logs-container" style="min-height: 100vh;" class="audit-logs-page bg-gray-50 dark:bg-gray-900">
|
||||||
|
<div class="pb-8 w-full">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-clipboard-list w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||||
|
Audit Logs
|
||||||
|
</h1>
|
||||||
|
<a href="/admin/audit/export" class="flex items-center justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-file-export w-4 h-4 mr-2"></i>
|
||||||
|
Export Logs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast container for notifications -->
|
||||||
|
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||||
|
|
||||||
|
<!-- Status and Error Messages -->
|
||||||
|
<div id="status-message" class="hidden mb-4 p-4 text-sm text-green-700 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-800" role="alert"></div>
|
||||||
|
<div id="error-message" class="hidden mb-4 p-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800" role="alert">
|
||||||
|
<div class="font-medium error-title"></div>
|
||||||
|
<div class="error-details mt-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 mb-6">
|
||||||
|
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Filter Logs</h3>
|
||||||
|
</div>
|
||||||
|
<div class="p-4">
|
||||||
|
<form method="GET" action="/admin/audit" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||||
|
<!-- Action filter -->
|
||||||
|
<div>
|
||||||
|
<label for="filter-action" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Action</label>
|
||||||
|
<select id="filter-action" name="action" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option value="">All Actions</option>
|
||||||
|
if data.FilterAction == "create" {
|
||||||
|
<option value="create" selected>Create</option>
|
||||||
|
} else {
|
||||||
|
<option value="create">Create</option>
|
||||||
|
}
|
||||||
|
if data.FilterAction == "update" {
|
||||||
|
<option value="update" selected>Update</option>
|
||||||
|
} else {
|
||||||
|
<option value="update">Update</option>
|
||||||
|
}
|
||||||
|
if data.FilterAction == "delete" {
|
||||||
|
<option value="delete" selected>Delete</option>
|
||||||
|
} else {
|
||||||
|
<option value="delete">Delete</option>
|
||||||
|
}
|
||||||
|
if data.FilterAction == "login" {
|
||||||
|
<option value="login" selected>Login</option>
|
||||||
|
} else {
|
||||||
|
<option value="login">Login</option>
|
||||||
|
}
|
||||||
|
if data.FilterAction == "logout" {
|
||||||
|
<option value="logout" selected>Logout</option>
|
||||||
|
} else {
|
||||||
|
<option value="logout">Logout</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Entity filter -->
|
||||||
|
<div>
|
||||||
|
<label for="filter-entity" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Entity Type</label>
|
||||||
|
<select id="filter-entity" name="entity" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option value="">All Entities</option>
|
||||||
|
if data.FilterEntity == "user" {
|
||||||
|
<option value="user" selected>User</option>
|
||||||
|
} else {
|
||||||
|
<option value="user">User</option>
|
||||||
|
}
|
||||||
|
if data.FilterEntity == "role" {
|
||||||
|
<option value="role" selected>Role</option>
|
||||||
|
} else {
|
||||||
|
<option value="role">Role</option>
|
||||||
|
}
|
||||||
|
if data.FilterEntity == "config" {
|
||||||
|
<option value="config" selected>Config</option>
|
||||||
|
} else {
|
||||||
|
<option value="config">Config</option>
|
||||||
|
}
|
||||||
|
if data.FilterEntity == "job" {
|
||||||
|
<option value="job" selected>Job</option>
|
||||||
|
} else {
|
||||||
|
<option value="job">Job</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User filter -->
|
||||||
|
<div>
|
||||||
|
<label for="filter-user" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
|
||||||
|
<input type="text" id="filter-user" name="user" value={data.FilterUser} placeholder="Filter by username" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date range -->
|
||||||
|
<div>
|
||||||
|
<label for="filter-date-from" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">From Date</label>
|
||||||
|
<input type="date" id="filter-date-from" name="date_from" value={data.FilterDateFrom} class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="filter-date-to" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">To Date</label>
|
||||||
|
<input type="date" id="filter-date-to" name="date_to" value={data.FilterDateTo} class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter buttons -->
|
||||||
|
<div class="lg:col-span-5 flex items-center justify-end gap-2">
|
||||||
|
<a href="/admin/audit" class="px-4 py-2 text-sm font-medium text-gray-900 bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
|
||||||
|
Clear Filters
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
|
Apply Filters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Audit Logs Table -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
<div class="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Audit Log Entries</h3>
|
||||||
|
<span class="text-sm text-gray-600 dark:text-gray-400">Total entries: {fmt.Sprint(data.TotalRecords)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Timestamp</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">User</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Action</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Entity</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Details</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
if len(data.Logs) == 0 {
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="px-6 py-4 text-center text-gray-500 dark:text-gray-400">
|
||||||
|
No audit logs found matching your filters.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
} else {
|
||||||
|
for _, log := range data.Logs {
|
||||||
|
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-900 dark:text-white">
|
||||||
|
{log.Timestamp.Format("2006-01-02 15:04:05")}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-900 dark:text-white">
|
||||||
|
{log.Username}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm">
|
||||||
|
<span class={getActionClass(log.Action)}>
|
||||||
|
{log.Action}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-900 dark:text-white">
|
||||||
|
{log.EntityType}
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
(ID: {fmt.Sprint(log.EntityID)})
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<button
|
||||||
|
data-details={log.DetailsSummary}
|
||||||
|
data-modal-target="details-modal"
|
||||||
|
data-modal-toggle="details-modal"
|
||||||
|
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 underline text-xs">
|
||||||
|
View Details
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
if data.TotalPages > 1 {
|
||||||
|
<div class="px-6 py-4 bg-gray-50 dark:bg-gray-700 border-t border-gray-200 dark:border-gray-600">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="text-sm text-gray-700 dark:text-gray-400">
|
||||||
|
Showing page {fmt.Sprint(data.CurrentPage)} of {fmt.Sprint(data.TotalPages)}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
if data.CurrentPage > 1 {
|
||||||
|
<a href={buildPaginationURL(data, data.CurrentPage-1)} class="px-3 py-1 text-sm text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
Previous
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Page numbers -->
|
||||||
|
<div class="hidden sm:flex space-x-1">
|
||||||
|
<!-- First page -->
|
||||||
|
if data.CurrentPage > 3 {
|
||||||
|
<a href={buildPaginationURL(data, 1)} class="px-3 py-1 text-sm text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
1
|
||||||
|
</a>
|
||||||
|
if data.CurrentPage > 4 {
|
||||||
|
<span class="px-3 py-1 text-sm text-gray-500 dark:text-gray-400">...</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Pages around current -->
|
||||||
|
for i := max(1, data.CurrentPage-2); i <= min(data.TotalPages, data.CurrentPage+2); i++ {
|
||||||
|
if i == data.CurrentPage {
|
||||||
|
<span class="px-3 py-1 text-sm text-white bg-blue-600 border border-blue-600 rounded-md dark:bg-blue-700">
|
||||||
|
{fmt.Sprint(i)}
|
||||||
|
</span>
|
||||||
|
} else {
|
||||||
|
<a href={buildPaginationURL(data, i)} class="px-3 py-1 text-sm text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
{fmt.Sprint(i)}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Last page -->
|
||||||
|
if data.CurrentPage < data.TotalPages-2 {
|
||||||
|
if data.CurrentPage < data.TotalPages-3 {
|
||||||
|
<span class="px-3 py-1 text-sm text-gray-500 dark:text-gray-400">...</span>
|
||||||
|
}
|
||||||
|
<a href={buildPaginationURL(data, data.TotalPages)} class="px-3 py-1 text-sm text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
{fmt.Sprint(data.TotalPages)}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
if data.CurrentPage < data.TotalPages {
|
||||||
|
<a href={buildPaginationURL(data, data.CurrentPage+1)} class="px-3 py-1 text-sm text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
Next
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Details Modal -->
|
||||||
|
<div id="details-modal" tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
|
||||||
|
<div class="relative p-4 w-full max-w-2xl max-h-full mx-auto">
|
||||||
|
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||||
|
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Audit Log Details
|
||||||
|
</h3>
|
||||||
|
<button type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" data-modal-hide="details-modal">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
<span class="sr-only">Close modal</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 md:p-5 space-y-4">
|
||||||
|
<pre id="details-content" class="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg text-sm text-gray-900 dark:text-gray-300 overflow-x-auto"></pre>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
|
||||||
|
<button data-modal-hide="details-modal" type="button" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Modal functionality
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const modal = document.getElementById('details-modal');
|
||||||
|
const detailsContent = document.getElementById('details-content');
|
||||||
|
|
||||||
|
// Handle modal show/hide
|
||||||
|
document.querySelectorAll('[data-modal-target="details-modal"]').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const details = this.getAttribute('data-details');
|
||||||
|
detailsContent.textContent = JSON.stringify(JSON.parse(details), null, 2);
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-modal-hide="details-modal"]').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal when clicking outside
|
||||||
|
modal.addEventListener('click', function(e) {
|
||||||
|
if (e.target === modal) {
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
func getActionClass(action string) string {
|
||||||
|
baseClass := "px-2 py-1 text-xs rounded-full "
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "create":
|
||||||
|
return baseClass + "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300"
|
||||||
|
case "update":
|
||||||
|
return baseClass + "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300"
|
||||||
|
case "delete":
|
||||||
|
return baseClass + "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300"
|
||||||
|
case "login":
|
||||||
|
return baseClass + "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300"
|
||||||
|
case "logout":
|
||||||
|
return baseClass + "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300"
|
||||||
|
default:
|
||||||
|
return baseClass + "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPaginationURL(data AuditLogsData, page int) templ.SafeURL {
|
||||||
|
url := fmt.Sprintf("/admin/audit?page=%d", page)
|
||||||
|
|
||||||
|
if data.FilterAction != "" {
|
||||||
|
url += fmt.Sprintf("&action=%s", data.FilterAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.FilterEntity != "" {
|
||||||
|
url += fmt.Sprintf("&entity=%s", data.FilterEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.FilterUser != "" {
|
||||||
|
url += fmt.Sprintf("&user=%s", data.FilterUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.FilterDateFrom != "" {
|
||||||
|
url += fmt.Sprintf("&date_from=%s", data.FilterDateFrom)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.FilterDateTo != "" {
|
||||||
|
url += fmt.Sprintf("&date_to=%s", data.FilterDateTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return templ.SafeURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,442 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BackupFile represents a database backup file
|
||||||
|
type BackupFile struct {
|
||||||
|
Name string
|
||||||
|
Size string
|
||||||
|
Created time.Time
|
||||||
|
FullPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminDatabaseTools renders the database tools page with backup and restore functionality
|
||||||
|
templ AdminDatabaseTools(ctx context.Context, backups []BackupFile) {
|
||||||
|
@LayoutWithContext("Database Tools", ctx) {
|
||||||
|
<div id="database-tools-container" style="min-height: 100vh;" class="database-tools-page bg-gray-50 dark:bg-gray-900">
|
||||||
|
<div class="pb-8 w-full">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-database w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||||
|
Database Tools
|
||||||
|
</h1>
|
||||||
|
<form id="backup-form" method="POST" action="/admin/database/backup-database">
|
||||||
|
<button type="submit" class="flex items-center justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-download w-4 h-4 mr-2"></i>
|
||||||
|
Create Backup
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status and Error Messages -->
|
||||||
|
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||||
|
|
||||||
|
<!-- Action Cards Grid -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||||
|
<!-- Optimize Database Card -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-6 dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div class="bg-purple-100 p-3 rounded-full dark:bg-purple-900 mr-4">
|
||||||
|
<i class="fas fa-broom text-purple-500 w-6 h-6 dark:text-purple-300"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Optimize Database</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mb-4">Vacuum the database to reclaim space and optimize performance.</p>
|
||||||
|
<form id="vacuum-form" method="POST" action="/admin/database/vacuum-database">
|
||||||
|
<button type="submit" class="w-full px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 dark:bg-purple-700 dark:hover:bg-purple-800">
|
||||||
|
<i class="fas fa-magic mr-2"></i>
|
||||||
|
Optimize Database
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Job History Card -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-6 dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div class="bg-orange-100 p-3 rounded-full dark:bg-orange-900 mr-4">
|
||||||
|
<i class="fas fa-eraser text-orange-500 w-6 h-6 dark:text-orange-300"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Clear Job History</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mb-4">Remove old job history records to free up database space.</p>
|
||||||
|
<form id="clear-job-form" method="POST" action="/admin/database/clear-job-history">
|
||||||
|
<button type="submit" class="w-full px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-orange-500 dark:bg-orange-700 dark:hover:bg-orange-800">
|
||||||
|
<i class="fas fa-trash-alt mr-2"></i>
|
||||||
|
Clear Job History
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Export Configuration Card -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-6 dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div class="bg-green-100 p-3 rounded-full dark:bg-green-900 mr-4">
|
||||||
|
<i class="fas fa-file-export text-green-500 w-6 h-6 dark:text-green-300"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Export Configuration</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mb-4">Export system configurations and jobs for backup or migration.</p>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<a href="/admin/database/export-configs" class="flex-1 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 dark:bg-green-700 dark:hover:bg-green-800 text-center">
|
||||||
|
<i class="fas fa-cogs mr-2"></i>
|
||||||
|
Export Configs
|
||||||
|
</a>
|
||||||
|
<a href="/admin/database/export-jobs" class="flex-1 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 dark:bg-green-700 dark:hover:bg-green-800 text-center">
|
||||||
|
<i class="fas fa-tasks mr-2"></i>
|
||||||
|
Export Jobs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Database Backup List -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
<div class="p-6 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Database Backups</h3>
|
||||||
|
<a href="/admin/database/refresh-backups" class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
|
||||||
|
<i class="fas fa-sync-alt mr-1"></i>
|
||||||
|
Refresh
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Backup Name</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Size</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Created</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
if len(backups) == 0 {
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="px-6 py-4 text-center text-gray-500 dark:text-gray-400">
|
||||||
|
No backups found. Create your first backup to get started.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
} else {
|
||||||
|
for _, backup := range backups {
|
||||||
|
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-900 dark:text-white">{ backup.Name }</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{ backup.Size }</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{ backup.Created.Format("2006-01-02 15:04:05") }</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-right">
|
||||||
|
<div class="flex items-center justify-end space-x-2">
|
||||||
|
<button type="button"
|
||||||
|
data-modal-target="confirm-restore-modal"
|
||||||
|
data-modal-toggle="confirm-restore-modal"
|
||||||
|
data-backup-name={ backup.Name }
|
||||||
|
class="text-yellow-600 hover:text-yellow-900 dark:text-yellow-400 dark:hover:text-yellow-300">
|
||||||
|
<i class="fas fa-undo-alt"></i>
|
||||||
|
</button>
|
||||||
|
<a href={ templ.SafeURL("/admin/database/download-backup/" + backup.Name) }
|
||||||
|
class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
|
</a>
|
||||||
|
<button type="button"
|
||||||
|
data-modal-target="confirm-delete-modal"
|
||||||
|
data-modal-toggle="confirm-delete-modal"
|
||||||
|
data-backup-name={ backup.Name }
|
||||||
|
class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload Backup Section -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 mt-8">
|
||||||
|
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Restore from Backup File</h3>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<form id="upload-backup-form" method="POST" action="/admin/database/restore-database" enctype="multipart/form-data">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Upload Backup File</label>
|
||||||
|
<div class="flex items-center justify-center w-full">
|
||||||
|
<label class="flex flex-col w-full h-32 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
|
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
||||||
|
<i class="fas fa-cloud-upload-alt text-3xl text-gray-400 mb-2"></i>
|
||||||
|
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400"><span class="font-semibold">Click to upload</span> or drag and drop</p>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">SQLite backup file</p>
|
||||||
|
</div>
|
||||||
|
<input type="file" name="backup_file" class="hidden" accept=".db,.sqlite,.backup" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end">
|
||||||
|
<button type="submit" class="px-4 py-2 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-yellow-500 dark:bg-yellow-700 dark:hover:bg-yellow-800">
|
||||||
|
<i class="fas fa-undo-alt mr-2"></i>
|
||||||
|
Restore Database
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Help Section -->
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg shadow-sm mt-8 p-4 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="flex items-start mb-2">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<i class="fas fa-info-circle w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 text-sm">
|
||||||
|
<h4 class="font-medium text-gray-900 dark:text-white">Important Safety Information</h4>
|
||||||
|
<ul class="mt-2 text-gray-600 dark:text-gray-400 space-y-1 list-disc list-inside">
|
||||||
|
<li>Always create a backup before making significant changes</li>
|
||||||
|
<li>Restoring a backup will overwrite all current data</li>
|
||||||
|
<li>Database optimization may take time for large databases</li>
|
||||||
|
<li>Clear job history operations cannot be undone</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Restore Confirmation Modal -->
|
||||||
|
<div id="confirm-restore-modal" tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
|
||||||
|
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
|
||||||
|
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||||
|
<div class="p-6 text-center">
|
||||||
|
<i class="fas fa-exclamation-triangle text-yellow-400 text-3xl mb-4"></i>
|
||||||
|
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">Are you sure you want to restore this backup? This will overwrite your current database.</h3>
|
||||||
|
<form method="POST" id="restore-form" action="/admin/database/restore-database">
|
||||||
|
<input type="hidden" name="filename" id="restore-filename" value="" />
|
||||||
|
<button type="submit" class="text-white bg-yellow-600 hover:bg-yellow-800 focus:ring-4 focus:outline-none focus:ring-yellow-300 dark:focus:ring-yellow-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center mr-2">
|
||||||
|
Yes, restore it
|
||||||
|
</button>
|
||||||
|
<button type="button" data-modal-hide="confirm-restore-modal" class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||||
|
No, cancel
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Modal -->
|
||||||
|
<div id="confirm-delete-modal" tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
|
||||||
|
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
|
||||||
|
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||||
|
<div class="p-6 text-center">
|
||||||
|
<i class="fas fa-trash-alt text-red-400 text-3xl mb-4"></i>
|
||||||
|
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">Are you sure you want to delete this backup file?</h3>
|
||||||
|
<form method="POST" id="delete-form" action="">
|
||||||
|
<button type="submit" class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center mr-2">
|
||||||
|
Yes, delete it
|
||||||
|
</button>
|
||||||
|
<button type="button" data-modal-hide="confirm-delete-modal" class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||||
|
No, cancel
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Handle restore modal
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Parse URL parameters for error and status messages
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const errorParam = urlParams.get('error');
|
||||||
|
const detailsParam = urlParams.get('details');
|
||||||
|
const statusParam = urlParams.get('status');
|
||||||
|
|
||||||
|
// Function to create and show a toast
|
||||||
|
function showToast(message, type) {
|
||||||
|
const toastContainer = document.getElementById('toast-container');
|
||||||
|
|
||||||
|
// Create toast element
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.id = 'toast-' + type + '-' + Date.now();
|
||||||
|
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
||||||
|
toast.role = 'alert';
|
||||||
|
|
||||||
|
// Set toast content based on type
|
||||||
|
let iconClass, bgColorClass, textColorClass;
|
||||||
|
|
||||||
|
if (type === 'success') {
|
||||||
|
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
||||||
|
bgColorClass = 'text-green-500 dark:text-green-200';
|
||||||
|
textColorClass = 'text-green-500 dark:text-green-200';
|
||||||
|
} else if (type === 'error') {
|
||||||
|
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
||||||
|
bgColorClass = 'text-red-500 dark:text-red-200';
|
||||||
|
textColorClass = 'text-red-500 dark:text-red-200';
|
||||||
|
} else {
|
||||||
|
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
||||||
|
bgColorClass = 'text-blue-500 dark:text-blue-200';
|
||||||
|
textColorClass = 'text-blue-500 dark:text-blue-200';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set inner HTML with appropriate icon and message
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
||||||
|
${type === 'success'
|
||||||
|
? '<i class="fas fa-check"></i>'
|
||||||
|
: type === 'error'
|
||||||
|
? '<i class="fas fa-exclamation-circle"></i>'
|
||||||
|
: '<i class="fas fa-info-circle"></i>'}
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 text-sm font-normal">${message}</div>
|
||||||
|
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add toast to container
|
||||||
|
toastContainer.appendChild(toast);
|
||||||
|
|
||||||
|
// Trigger animation after a small delay to ensure the DOM has updated
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('translate-y-16', 'opacity-0');
|
||||||
|
toast.classList.add('translate-y-0', 'opacity-100');
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// Add event listener to close button
|
||||||
|
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
||||||
|
closeButton.addEventListener('click', function() {
|
||||||
|
// Animate out before removing
|
||||||
|
toast.classList.add('opacity-0', 'translate-y-4');
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-remove toast after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.add('opacity-0', 'translate-y-4');
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display error message if present
|
||||||
|
if (errorParam) {
|
||||||
|
const errorMessage = decodeURIComponent(errorParam).replace(/\+/g, ' ');
|
||||||
|
let details = '';
|
||||||
|
|
||||||
|
if (detailsParam) {
|
||||||
|
details = decodeURIComponent(detailsParam).replace(/\+/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullMessage = details ? `${errorMessage}: ${details}` : errorMessage;
|
||||||
|
showToast(fullMessage, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display status message if present
|
||||||
|
if (statusParam) {
|
||||||
|
const statusMessage = decodeURIComponent(statusParam).replace(/\+/g, ' ');
|
||||||
|
showToast(statusMessage, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display modals when buttons are clicked
|
||||||
|
const restoreButtons = document.querySelectorAll('[data-modal-target="confirm-restore-modal"]');
|
||||||
|
restoreButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const backupName = this.getAttribute('data-backup-name');
|
||||||
|
document.getElementById('restore-filename').value = backupName;
|
||||||
|
document.getElementById('restore-form').action = `/admin/database/restore-database/${backupName}`;
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
|
const modal = document.getElementById('confirm-restore-modal');
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
modal.classList.add('flex');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle delete modal
|
||||||
|
const deleteButtons = document.querySelectorAll('[data-modal-target="confirm-delete-modal"]');
|
||||||
|
deleteButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const backupName = this.getAttribute('data-backup-name');
|
||||||
|
document.getElementById('delete-form').action = `/admin/database/delete-backup/${backupName}`;
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
|
const modal = document.getElementById('confirm-delete-modal');
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
modal.classList.add('flex');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle modal hide buttons
|
||||||
|
const hideButtons = document.querySelectorAll('[data-modal-hide]');
|
||||||
|
hideButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const modalId = this.getAttribute('data-modal-hide');
|
||||||
|
const modal = document.getElementById(modalId);
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// File upload preview
|
||||||
|
const fileInput = document.querySelector('input[type="file"]');
|
||||||
|
if (fileInput) {
|
||||||
|
fileInput.addEventListener('change', function(e) {
|
||||||
|
const fileName = e.target.files[0]?.name || 'No file selected';
|
||||||
|
const fileLabel = this.parentElement.querySelector('p.mb-2');
|
||||||
|
if (fileLabel) {
|
||||||
|
fileLabel.textContent = fileName;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add form submission events
|
||||||
|
const backupForm = document.getElementById('backup-form');
|
||||||
|
if (backupForm) {
|
||||||
|
backupForm.addEventListener('submit', function(e) {
|
||||||
|
console.log('Backup form submitted');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const vacuumForm = document.getElementById('vacuum-form');
|
||||||
|
if (vacuumForm) {
|
||||||
|
vacuumForm.addEventListener('submit', function(e) {
|
||||||
|
console.log('Vacuum form submitted');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearJobForm = document.getElementById('clear-job-form');
|
||||||
|
if (clearJobForm) {
|
||||||
|
clearJobForm.addEventListener('submit', function(e) {
|
||||||
|
console.log('Clear job form submitted');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set dark background color if in dark mode
|
||||||
|
if (document.documentElement.classList.contains('dark')) {
|
||||||
|
document.getElementById('database-tools-container').style.backgroundColor = '#111827';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener for theme changes
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener('click', function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
document.getElementById('database-tools-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RoleFormData struct {
|
||||||
|
Role *Role
|
||||||
|
IsNew bool
|
||||||
|
ErrorMessage string
|
||||||
|
AllPermissions []string
|
||||||
|
}
|
||||||
|
|
||||||
|
templ AdminRoleForm(ctx context.Context, data RoleFormData) {
|
||||||
|
@LayoutWithContext(getFormTitle(data.IsNew), ctx) {
|
||||||
|
<div class="mb-6">
|
||||||
|
<nav class="flex" aria-label="Breadcrumb">
|
||||||
|
<ol class="inline-flex items-center space-x-1 md:space-x-3">
|
||||||
|
<li class="inline-flex items-center">
|
||||||
|
<a href="/admin" class="inline-flex items-center text-sm font-medium text-gray-700 hover:text-blue-600 dark:text-gray-400 dark:hover:text-white">
|
||||||
|
<i class="fas fa-shield-alt w-4 h-4 mr-2"></i>
|
||||||
|
Admin
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-chevron-right w-3 h-3 text-gray-400 mx-1"></i>
|
||||||
|
<a href="/admin/roles" class="ml-1 text-sm font-medium text-gray-700 hover:text-blue-600 md:ml-2 dark:text-gray-400 dark:hover:text-white">Roles</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li aria-current="page">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-chevron-right w-3 h-3 text-gray-400 mx-1"></i>
|
||||||
|
<span class="ml-1 text-sm font-medium text-gray-500 md:ml-2 dark:text-gray-400">
|
||||||
|
if data.IsNew {
|
||||||
|
New Role
|
||||||
|
} else {
|
||||||
|
Edit Role
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
if data.ErrorMessage != "" {
|
||||||
|
<div class="p-4 mb-6 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-red-900/50 dark:text-red-400" role="alert">
|
||||||
|
<span class="font-medium">Error!</span> { data.ErrorMessage }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow-sm dark:bg-gray-800">
|
||||||
|
<div class="p-6 space-y-6">
|
||||||
|
<form method="POST" action={ getFormAction(data.IsNew, data.Role) }>
|
||||||
|
if !data.IsNew {
|
||||||
|
<input type="hidden" name="_method" value="PUT"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Role Name -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Role Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||||
|
placeholder="Enter role name"
|
||||||
|
required
|
||||||
|
if !data.IsNew {
|
||||||
|
value={ data.Role.Name }
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Role Description -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="description" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
rows="3"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||||
|
placeholder="Enter role description"
|
||||||
|
>
|
||||||
|
if !data.IsNew {
|
||||||
|
{ data.Role.Description }
|
||||||
|
}
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Permissions -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Permissions</label>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
for _, perm := range data.AllPermissions {
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={ "perm_" + perm }
|
||||||
|
name="permissions[]"
|
||||||
|
value={ perm }
|
||||||
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
if !data.IsNew && hasPermission(data.Role, perm) {
|
||||||
|
checked
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label for={ "perm_" + perm } class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300">
|
||||||
|
{ formatPermissionLabel(perm) }
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form Actions -->
|
||||||
|
<div class="flex items-center justify-end space-x-3">
|
||||||
|
<a href="/admin/roles" class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
|
if data.IsNew {
|
||||||
|
Create Role
|
||||||
|
} else {
|
||||||
|
Update Role
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
func getFormTitle(isNew bool) string {
|
||||||
|
if isNew {
|
||||||
|
return "New Role"
|
||||||
|
}
|
||||||
|
return "Edit Role"
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFormAction(isNew bool, role *Role) templ.SafeURL {
|
||||||
|
if isNew {
|
||||||
|
return "/admin/roles"
|
||||||
|
}
|
||||||
|
return templ.SafeURL("/admin/roles/" + fmt.Sprint(role.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasPermission(role *Role, permission string) bool {
|
||||||
|
if role == nil || role.Permissions == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, p := range role.Permissions {
|
||||||
|
if p == permission {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatPermissionLabel(permission string) string {
|
||||||
|
// Convert permission strings like "users.create" to "Create Users"
|
||||||
|
parts := strings.Split(permission, ".")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return permission
|
||||||
|
}
|
||||||
|
|
||||||
|
resource := strings.Title(parts[0])
|
||||||
|
action := strings.Title(parts[1])
|
||||||
|
|
||||||
|
return action + " " + resource
|
||||||
|
}
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Role struct {
|
||||||
|
ID uint
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Permissions []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RolesData struct {
|
||||||
|
Roles []Role
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
templ AdminRoles(ctx context.Context, data RolesData) {
|
||||||
|
@LayoutWithContext("Role Management", ctx) {
|
||||||
|
<!-- Toast container for status messages -->
|
||||||
|
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||||
|
|
||||||
|
<!-- Roles Management Content -->
|
||||||
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-user-shield w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||||
|
Role Management
|
||||||
|
</h1>
|
||||||
|
<a href="/admin/roles/new" class="flex items-center justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||||
|
New Role
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Roles Table -->
|
||||||
|
<div class="bg-white rounded-lg shadow-sm dark:bg-gray-800">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Role Name</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Description</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Permissions</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
if len(data.Roles) == 0 {
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="px-6 py-4 text-center text-gray-500 dark:text-gray-400">
|
||||||
|
No roles found. Create your first role to get started.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
} else {
|
||||||
|
for _, role := range data.Roles {
|
||||||
|
<!-- Dialog for this role -->
|
||||||
|
@RoleDialog(
|
||||||
|
fmt.Sprintf("delete-role-dialog-%d", role.ID),
|
||||||
|
"Delete Role",
|
||||||
|
fmt.Sprintf("Are you sure you want to delete role \"%s\"? This action cannot be undone.", role.Name),
|
||||||
|
"bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700",
|
||||||
|
"Delete Role",
|
||||||
|
"delete",
|
||||||
|
role.ID,
|
||||||
|
role.Name,
|
||||||
|
)
|
||||||
|
|
||||||
|
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-900 dark:text-white">{ role.Name }</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{ role.Description }</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
for _, perm := range role.Permissions {
|
||||||
|
<span class="px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300">
|
||||||
|
{ perm }
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-right">
|
||||||
|
<div class="flex items-center justify-end space-x-2">
|
||||||
|
<a href={ templ.SafeURL("/admin/roles/" + fmt.Sprint(role.ID)) } class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
data-role-id={ fmt.Sprint(role.ID) }
|
||||||
|
data-role-name={ role.Name }
|
||||||
|
onclick={ showModal(fmt.Sprintf("delete-role-dialog-%d", role.ID)) }
|
||||||
|
class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Function to create and show a toast
|
||||||
|
function showToast(message, type) {
|
||||||
|
const toastContainer = document.getElementById('toast-container');
|
||||||
|
|
||||||
|
// Create toast element
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.id = 'toast-' + type + '-' + Date.now();
|
||||||
|
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
||||||
|
toast.role = 'alert';
|
||||||
|
|
||||||
|
// Set toast content based on type
|
||||||
|
let iconClass, bgColorClass, textColorClass;
|
||||||
|
|
||||||
|
if (type === 'success') {
|
||||||
|
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
||||||
|
bgColorClass = 'text-green-500 dark:text-green-200';
|
||||||
|
textColorClass = 'text-green-500 dark:text-green-200';
|
||||||
|
} else if (type === 'error') {
|
||||||
|
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
||||||
|
bgColorClass = 'text-red-500 dark:text-red-200';
|
||||||
|
textColorClass = 'text-red-500 dark:text-red-200';
|
||||||
|
} else {
|
||||||
|
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
||||||
|
bgColorClass = 'text-blue-500 dark:text-blue-200';
|
||||||
|
textColorClass = 'text-blue-500 dark:text-blue-200';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set inner HTML with appropriate icon and message
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
||||||
|
${type === 'success'
|
||||||
|
? '<i class="fas fa-check"></i>'
|
||||||
|
: type === 'error'
|
||||||
|
? '<i class="fas fa-exclamation-circle"></i>'
|
||||||
|
: '<i class="fas fa-info-circle"></i>'}
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 text-sm font-normal">${message}</div>
|
||||||
|
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add toast to container
|
||||||
|
toastContainer.appendChild(toast);
|
||||||
|
|
||||||
|
// Trigger animation after a small delay to ensure the DOM has updated
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('translate-y-16', 'opacity-0');
|
||||||
|
toast.classList.add('translate-y-0', 'opacity-100');
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// Add event listener to close button
|
||||||
|
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
||||||
|
closeButton.addEventListener('click', function() {
|
||||||
|
// Animate out before removing
|
||||||
|
toast.classList.add('opacity-0', 'translate-y-4');
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-remove toast after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.add('opacity-0', 'translate-y-4');
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle modal hide buttons
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const hideButtons = document.querySelectorAll('[data-modal-hide]');
|
||||||
|
hideButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const modalId = this.getAttribute('data-modal-hide');
|
||||||
|
const modal = document.getElementById(modalId);
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for URL parameters after page load to show success/error messages
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const successMsg = urlParams.get('success');
|
||||||
|
const errorMsg = urlParams.get('error');
|
||||||
|
|
||||||
|
// Show success toast if parameter exists
|
||||||
|
if (successMsg) {
|
||||||
|
showToast(decodeURIComponent(successMsg), 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error toast if parameter exists
|
||||||
|
if (errorMsg) {
|
||||||
|
showToast(decodeURIComponent(errorMsg), 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error from the data if it exists
|
||||||
|
if (document.querySelector('[data-error-message]')) {
|
||||||
|
const errorMsg = document.querySelector('[data-error-message]').getAttribute('data-error-message');
|
||||||
|
if (errorMsg) {
|
||||||
|
showToast(errorMsg, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up window variables to track deletions
|
||||||
|
window.lastDeletedRole = null;
|
||||||
|
window.isRoleDeletingRequest = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle role deletion
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const confirmDeleteButtons = document.querySelectorAll('[id^="delete-role-btn-"]');
|
||||||
|
confirmDeleteButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const roleId = this.getAttribute('data-role-id');
|
||||||
|
const roleName = this.getAttribute('data-role-name');
|
||||||
|
|
||||||
|
// Store deletion info for event tracking
|
||||||
|
window.lastDeletedRole = {
|
||||||
|
id: roleId,
|
||||||
|
name: roleName
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send DELETE request
|
||||||
|
fetch(`/admin/roles/${roleId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
// Show success toast
|
||||||
|
showToast(`Role "${roleName}" deleted successfully`, 'success');
|
||||||
|
// Reload page on success after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
// Handle error
|
||||||
|
return response.json().then(data => {
|
||||||
|
showToast(`Error: ${data.error || 'Failed to delete role'}`, 'error');
|
||||||
|
}).catch(() => {
|
||||||
|
showToast(`Error deleting role "${roleName}"`, 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showToast(`Error: ${error.message || 'Failed to connect to server'}`, 'error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Store error message if it exists -->
|
||||||
|
if data.Error != "" {
|
||||||
|
<div data-error-message={ data.Error } class="hidden"></div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoleDialog for confirmation actions
|
||||||
|
templ RoleDialog(id string, title string, message string, confirmClass string, confirmText string, action string, roleID uint, roleName string) {
|
||||||
|
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
|
||||||
|
<!-- Modal content -->
|
||||||
|
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
|
||||||
|
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||||
|
<div class="p-6 text-center">
|
||||||
|
if action == "delete" {
|
||||||
|
<i class="fas fa-trash-alt text-red-400 text-3xl mb-4"></i>
|
||||||
|
} else {
|
||||||
|
<i class="fas fa-exclamation-triangle text-yellow-400 text-3xl mb-4"></i>
|
||||||
|
}
|
||||||
|
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{ message }</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-white font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 bg-red-600 hover:bg-red-700 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-500 dark:hover:bg-red-600 dark:focus:ring-red-800"
|
||||||
|
id={ fmt.Sprintf("delete-role-btn-%d", roleID) }
|
||||||
|
data-role-id={ fmt.Sprint(roleID) }
|
||||||
|
data-role-name={ roleName }
|
||||||
|
onclick={ triggerRoleDelete(id, roleID, roleName) }>
|
||||||
|
{ confirmText }
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick={ closeModal(id) } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
script triggerRoleDelete(dialogId string, roleID uint, roleName string) {
|
||||||
|
// Hide the dialog
|
||||||
|
document.getElementById(dialogId).classList.add("hidden");
|
||||||
|
document.getElementById(dialogId).classList.remove("flex");
|
||||||
|
|
||||||
|
// Store data in a way that's accessible to event handlers
|
||||||
|
window.lastDeletedRole = {
|
||||||
|
id: roleID,
|
||||||
|
name: roleName
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add custom marker to track this deletion
|
||||||
|
window.isRoleDeletingRequest = true;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,655 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"encoding/json"
|
||||||
|
"context"
|
||||||
|
"github.com/starfleetcptn/gomft/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ AuthProviderForm(ctx context.Context, provider *db.AuthProvider, isNew bool) {
|
||||||
|
@LayoutWithContext(getPageTitle(isNew), ctx) {
|
||||||
|
<div id="auth-provider-form-container" style="min-height: 100vh; background-color: rgb(249, 250, 251);" class="auth-provider-form-page bg-gray-50 dark:bg-gray-900">
|
||||||
|
<div class="p-4 pb-8 w-full">
|
||||||
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
|
||||||
|
<div>
|
||||||
|
if isNew {
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-user-shield w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||||
|
New Authentication Provider
|
||||||
|
</h1>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">Configure a new external authentication source</p>
|
||||||
|
} else {
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-user-shield w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||||
|
Edit Authentication Provider
|
||||||
|
</h1>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">Update an existing external authentication source</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/admin/settings/auth-providers" class="text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-4 py-2 text-center inline-flex items-center dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 dark:focus:ring-gray-700">
|
||||||
|
<i class="fas fa-arrow-left w-4 h-4 mr-2"></i> Back to Providers
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 p-6">
|
||||||
|
if isNew {
|
||||||
|
<form method="POST" action="/admin/settings/auth-providers" class="space-y-6">
|
||||||
|
@formContent(provider, isNew)
|
||||||
|
</form>
|
||||||
|
} else {
|
||||||
|
<form method="POST" action={ templ.SafeURL(fmt.Sprintf("/admin/settings/auth-providers/%d", provider.ID)) } class="space-y-6">
|
||||||
|
<input type="hidden" name="_method" value="PUT" />
|
||||||
|
@formContent(provider, isNew)
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Help Section -->
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg shadow-sm mt-8 p-4 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="flex items-start mb-2">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<i class="fas fa-info-circle w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 text-sm">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">Authentication providers allow users to sign in using external identity providers.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start mt-4">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<i class="fas fa-key w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 text-sm">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">Make sure to enter the correct callback URL in your external provider's configuration.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start mt-4">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<i class="fas fa-user-check w-4 h-4 text-indigo-500 dark:text-indigo-400 mr-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 text-sm">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">Configure attribute mappings to match the fields in your identity provider's user data.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function updateFormFields() {
|
||||||
|
// Hide all provider-specific sections first
|
||||||
|
document.querySelectorAll('.provider-specific-section').forEach(section => {
|
||||||
|
section.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show the relevant section based on selected provider type
|
||||||
|
const providerType = document.getElementById('type').value;
|
||||||
|
|
||||||
|
if (providerType === 'authentik') {
|
||||||
|
document.getElementById('authentikSection').style.display = 'block';
|
||||||
|
} else if (providerType === 'oidc') {
|
||||||
|
document.getElementById('oidcSection').style.display = 'block';
|
||||||
|
} else if (providerType === 'saml') {
|
||||||
|
document.getElementById('samlSection').style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testProviderConnection() {
|
||||||
|
const providerId = window.location.pathname.split('/').slice(-2)[0];
|
||||||
|
|
||||||
|
const testBtn = event.target;
|
||||||
|
const originalText = testBtn.innerHTML;
|
||||||
|
testBtn.disabled = true;
|
||||||
|
testBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Testing...';
|
||||||
|
|
||||||
|
fetch(`/admin/settings/auth-providers/${providerId}/test`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
showToast('error', data.error);
|
||||||
|
} else if (data.success) {
|
||||||
|
showToast('success', data.message || 'Connection test successful');
|
||||||
|
} else {
|
||||||
|
showToast('error', 'Connection test failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
testBtn.disabled = false;
|
||||||
|
testBtn.innerHTML = originalText;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showToast('error', 'Connection test failed');
|
||||||
|
|
||||||
|
testBtn.disabled = false;
|
||||||
|
testBtn.innerHTML = originalText;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle icon URL preview
|
||||||
|
function updateIconPreview() {
|
||||||
|
const iconUrl = document.getElementById('icon_url').value;
|
||||||
|
const previewImg = document.getElementById('preview-img');
|
||||||
|
const previewPlaceholder = document.getElementById('preview-placeholder');
|
||||||
|
|
||||||
|
if (iconUrl && iconUrl.trim() !== '') {
|
||||||
|
// Create image element if it doesn't exist
|
||||||
|
if (!previewImg) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.id = 'preview-img';
|
||||||
|
img.className = 'max-w-full max-h-full';
|
||||||
|
img.onerror = function() {
|
||||||
|
// If image fails to load, show placeholder
|
||||||
|
this.style.display = 'none';
|
||||||
|
if (previewPlaceholder) {
|
||||||
|
previewPlaceholder.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
const icon = document.createElement('i');
|
||||||
|
icon.id = 'preview-placeholder';
|
||||||
|
icon.className = 'fas fa-exclamation-circle text-red-500 text-xl';
|
||||||
|
document.getElementById('icon-preview').appendChild(icon);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
img.onload = function() {
|
||||||
|
// If image loads successfully, hide placeholder
|
||||||
|
this.style.display = 'block';
|
||||||
|
if (previewPlaceholder) {
|
||||||
|
previewPlaceholder.style.display = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.getElementById('icon-preview').appendChild(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update image source
|
||||||
|
if (previewImg) {
|
||||||
|
previewImg.src = iconUrl;
|
||||||
|
previewImg.style.display = 'block';
|
||||||
|
if (previewPlaceholder) {
|
||||||
|
previewPlaceholder.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If no URL, show placeholder
|
||||||
|
if (previewImg) {
|
||||||
|
previewImg.style.display = 'none';
|
||||||
|
}
|
||||||
|
if (previewPlaceholder) {
|
||||||
|
previewPlaceholder.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the form on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
updateFormFields();
|
||||||
|
|
||||||
|
// Set dark background color if in dark mode
|
||||||
|
if (document.documentElement.classList.contains('dark')) {
|
||||||
|
document.getElementById('auth-provider-form-container').style.backgroundColor = '#111827';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener for theme changes
|
||||||
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener('click', function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
document.getElementById('auth-provider-form-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener for icon URL changes
|
||||||
|
const iconUrlInput = document.getElementById('icon_url');
|
||||||
|
if (iconUrlInput) {
|
||||||
|
iconUrlInput.addEventListener('input', updateIconPreview);
|
||||||
|
// Initial preview
|
||||||
|
updateIconPreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPageTitle(isNew bool) string {
|
||||||
|
if isNew {
|
||||||
|
return "New Authentication Provider"
|
||||||
|
}
|
||||||
|
return "Edit Authentication Provider"
|
||||||
|
}
|
||||||
|
|
||||||
|
templ formContent(provider *db.AuthProvider, isNew bool) {
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
|
<!-- Name -->
|
||||||
|
<div>
|
||||||
|
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Name <span class="text-red-600">*</span></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="e.g. Authentik SSO"
|
||||||
|
required
|
||||||
|
value={ getValue(provider, "name") }
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">A descriptive name for this authentication provider</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Provider Type -->
|
||||||
|
<div>
|
||||||
|
<label for="type" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Provider Type <span class="text-red-600">*</span></label>
|
||||||
|
<select id="type" name="type" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required onchange="updateFormFields()">
|
||||||
|
<option value="">Select a provider type</option>
|
||||||
|
if provider != nil && provider.Type == db.ProviderTypeAuthentik {
|
||||||
|
<option value="authentik" selected>Authentik</option>
|
||||||
|
} else {
|
||||||
|
<option value="authentik">Authentik</option>
|
||||||
|
}
|
||||||
|
if provider != nil && provider.Type == db.ProviderTypeOIDC {
|
||||||
|
<option value="oidc" selected>OpenID Connect (OIDC)</option>
|
||||||
|
} else {
|
||||||
|
<option value="oidc">OpenID Connect (OIDC)</option>
|
||||||
|
}
|
||||||
|
if provider != nil && provider.Type == db.ProviderTypeSAML {
|
||||||
|
<option value="saml" selected>SAML 2.0</option>
|
||||||
|
} else {
|
||||||
|
<option value="saml">SAML 2.0</option>
|
||||||
|
}
|
||||||
|
if provider != nil && provider.Type == db.ProviderTypeOAuth2 {
|
||||||
|
<option value="oauth2" selected>OAuth 2.0</option>
|
||||||
|
} else {
|
||||||
|
<option value="oauth2">OAuth 2.0</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">The type of external authentication service</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Enabled -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
if provider == nil || provider.GetEnabled() {
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="enabled"
|
||||||
|
name="enabled"
|
||||||
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
} else {
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="enabled"
|
||||||
|
name="enabled"
|
||||||
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
<label for="enabled" class="ml-2 block text-sm font-medium text-gray-700 dark:text-gray-300">Enabled</label>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Whether this authentication provider is active and available for login</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<div>
|
||||||
|
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Description</label>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Optional description of this provider"
|
||||||
|
>
|
||||||
|
if provider != nil {
|
||||||
|
{ provider.Description }
|
||||||
|
}
|
||||||
|
</textarea>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Additional information about this authentication provider</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Icon URL -->
|
||||||
|
<div class="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Provider Icon</h3>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label for="icon_url" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Icon URL</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="icon_url"
|
||||||
|
name="icon_url"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="https://example.com/icon.svg"
|
||||||
|
value={ provider.IconURL }
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">URL to the provider's icon image (SVG recommended)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Icon Preview</label>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div id="icon-preview" class="border border-gray-300 dark:border-gray-700 rounded-lg p-4 flex items-center justify-center w-20 h-20 bg-white dark:bg-gray-800">
|
||||||
|
if provider != nil && provider.IconURL != "" {
|
||||||
|
<img src={ provider.IconURL } class="max-w-full max-h-full" id="preview-img" />
|
||||||
|
} else {
|
||||||
|
<i class="fas fa-image text-gray-400 text-xl" id="preview-placeholder"></i>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">Preview of the icon that will be displayed on the login button.</p>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">If no URL is provided, a default icon will be used based on the provider type.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Connection Settings</h3>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<!-- Provider URL -->
|
||||||
|
<div>
|
||||||
|
<label for="provider_url" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Provider URL <span class="text-red-600">*</span></label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="provider_url"
|
||||||
|
name="provider_url"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="https://authentik.example.com"
|
||||||
|
required
|
||||||
|
value={ getValue(provider, "provider_url") }
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">The base URL of the authentication provider</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Client ID -->
|
||||||
|
<div>
|
||||||
|
<label for="client_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Client ID <span class="text-red-600">*</span></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="client_id"
|
||||||
|
name="client_id"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
required
|
||||||
|
value={ getValue(provider, "client_id") }
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">The client identifier assigned by the authentication provider</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Client Secret -->
|
||||||
|
<div>
|
||||||
|
<label for="client_secret" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Client Secret
|
||||||
|
if !isNew {
|
||||||
|
<span class="text-gray-500 text-xs font-normal ml-2">(leave empty to keep current)</span>
|
||||||
|
} else {
|
||||||
|
<span class="text-red-600">*</span>
|
||||||
|
}
|
||||||
|
</label>
|
||||||
|
if isNew {
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="client_secret"
|
||||||
|
name="client_secret"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
} else {
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="client_secret"
|
||||||
|
name="client_secret"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">The client secret for authentication with the provider</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Redirect URL -->
|
||||||
|
<div>
|
||||||
|
<label for="redirect_url" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Redirect URL <span class="text-red-600">*</span></label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="redirect_url"
|
||||||
|
name="redirect_url"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="https://your-app.example.com/auth/callback"
|
||||||
|
required
|
||||||
|
value={ getRedirectURL(provider) }
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">The callback URL that will handle the authentication response</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scopes -->
|
||||||
|
<div>
|
||||||
|
<label for="scopes" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Scopes</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="scopes"
|
||||||
|
name="scopes"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="openid profile email"
|
||||||
|
value={ getValue(provider, "scopes") }
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Space-separated list of scopes to request from the provider</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-gray-200 dark:border-gray-700 pt-6 provider-specific-section" id="authentikSection">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Authentik Settings</h3>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<!-- Authentik Tenant ID -->
|
||||||
|
<div>
|
||||||
|
<label for="authentik_tenant" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Tenant ID</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="authentik_tenant"
|
||||||
|
name="authentik_tenant"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="default"
|
||||||
|
value={ getConfigValue(provider, "tenant_id") }
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Authentik tenant ID (optional, defaults to 'default')</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-gray-200 dark:border-gray-700 pt-6 provider-specific-section" id="oidcSection">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">OpenID Connect Settings</h3>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<!-- OIDC Discovery URL -->
|
||||||
|
<div>
|
||||||
|
<label for="oidc_discovery_url" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Discovery URL</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="oidc_discovery_url"
|
||||||
|
name="oidc_discovery_url"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="https://provider/.well-known/openid-configuration"
|
||||||
|
value={ getConfigValue(provider, "discovery_url") }
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">URL to the OIDC discovery document</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-gray-200 dark:border-gray-700 pt-6 provider-specific-section" id="samlSection">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">SAML Settings</h3>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-6">
|
||||||
|
<!-- SAML Metadata URL -->
|
||||||
|
<div>
|
||||||
|
<label for="saml_metadata_url" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Metadata URL</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="saml_metadata_url"
|
||||||
|
name="saml_metadata_url"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="https://provider/metadata.xml"
|
||||||
|
value={ getConfigValue(provider, "metadata_url") }
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">URL to the SAML metadata XML</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">User Attribute Mapping</h3>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<!-- Username Attribute -->
|
||||||
|
<div>
|
||||||
|
<label for="attr_username" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Username Attribute</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="attr_username"
|
||||||
|
name="attr_username"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="preferred_username"
|
||||||
|
value={ getAttributeValue(provider, "username") }
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">The attribute to use as the username</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email Attribute -->
|
||||||
|
<div>
|
||||||
|
<label for="attr_email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Email Attribute</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="attr_email"
|
||||||
|
name="attr_email"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="email"
|
||||||
|
value={ getAttributeValue(provider, "email") }
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">The attribute to use as the email address</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Display Name Attribute -->
|
||||||
|
<div>
|
||||||
|
<label for="attr_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Display Name Attribute</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="attr_name"
|
||||||
|
name="attr_name"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="name"
|
||||||
|
value={ getAttributeValue(provider, "name") }
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">The attribute to use as the display name</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Groups Attribute -->
|
||||||
|
<div>
|
||||||
|
<label for="attr_groups" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Groups Attribute</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="attr_groups"
|
||||||
|
name="attr_groups"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="groups"
|
||||||
|
value={ getAttributeValue(provider, "groups") }
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">The attribute that contains user groups</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-4 mt-6">
|
||||||
|
<a href="/admin/settings/auth-providers" class="text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 dark:focus:ring-gray-700">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
if !isNew {
|
||||||
|
<button type="button" class="text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center dark:bg-blue-500 dark:hover:bg-blue-600 dark:focus:ring-blue-700" onclick="testProviderConnection()">
|
||||||
|
<i class="fas fa-check-circle w-4 h-4 mr-2"></i>
|
||||||
|
Test Connection
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
|
if isNew {
|
||||||
|
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||||
|
Create Provider
|
||||||
|
} else {
|
||||||
|
<i class="fas fa-save w-4 h-4 mr-2"></i>
|
||||||
|
Save Changes
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
func getValue(provider *db.AuthProvider, field string) string {
|
||||||
|
if provider == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch field {
|
||||||
|
case "name":
|
||||||
|
return provider.Name
|
||||||
|
case "provider_url":
|
||||||
|
return provider.ProviderURL
|
||||||
|
case "client_id":
|
||||||
|
return provider.ClientID
|
||||||
|
case "redirect_url":
|
||||||
|
return provider.RedirectURL
|
||||||
|
case "scopes":
|
||||||
|
return provider.Scopes
|
||||||
|
case "icon_url":
|
||||||
|
return provider.IconURL
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRedirectURL(provider *db.AuthProvider) string {
|
||||||
|
if provider == nil || provider.RedirectURL == "" {
|
||||||
|
return fmt.Sprintf("https://%s/auth/callback", "your-app-domain.com")
|
||||||
|
}
|
||||||
|
return provider.RedirectURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConfigValue(provider *db.AuthProvider, key string) string {
|
||||||
|
if provider == nil || provider.Config == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var config map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(provider.Config), &config); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := config[key]; ok {
|
||||||
|
if strValue, ok := value.(string); ok {
|
||||||
|
return strValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAttributeValue(provider *db.AuthProvider, key string) string {
|
||||||
|
if provider == nil || provider.AttributeMapping == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var mapping map[string]string
|
||||||
|
if err := json.Unmarshal([]byte(provider.AttributeMapping), &mapping); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := mapping[key]; ok {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,380 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
"github.com/starfleetcptn/gomft/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// formatTime formats a time.Time value as a human-readable string
|
||||||
|
func formatTime(t time.Time) string {
|
||||||
|
return t.Format("Jan 02, 2006 15:04")
|
||||||
|
}
|
||||||
|
|
||||||
|
templ AuthProviders(ctx context.Context, providers []db.AuthProvider) {
|
||||||
|
@LayoutWithContext("Authentication Providers", ctx) {
|
||||||
|
<!-- Toast container for notifications -->
|
||||||
|
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||||
|
|
||||||
|
<div id="auth-providers-container" style="min-height: 100vh; background-color: rgb(249, 250, 251);" class="auth-providers-page bg-gray-50 dark:bg-gray-900">
|
||||||
|
<div class="p-4 pb-8 w-full">
|
||||||
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-user-shield w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||||
|
Authentication Providers
|
||||||
|
</h1>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">Manage external authentication sources like Authentik, OIDC, SAML, etc.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/admin/settings/auth-providers/new" class="flex items-center justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||||
|
New Provider
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
if len(providers) > 0 {
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 overflow-hidden">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm text-left rtl:text-right">
|
||||||
|
<thead class="text-xs uppercase bg-gray-100 dark:bg-gray-700">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="px-6 py-3">Name</th>
|
||||||
|
<th scope="col" class="px-6 py-3">Icon</th>
|
||||||
|
<th scope="col" class="px-6 py-3">Type</th>
|
||||||
|
<th scope="col" class="px-6 py-3">Status</th>
|
||||||
|
<th scope="col" class="px-6 py-3">Provider URL</th>
|
||||||
|
<th scope="col" class="px-6 py-3">Last Used</th>
|
||||||
|
<th scope="col" class="px-6 py-3">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
for _, provider := range providers {
|
||||||
|
<tr class="border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
||||||
|
<td class="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap">
|
||||||
|
{ provider.Name }
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
if provider.IconURL != "" {
|
||||||
|
<img src={ provider.IconURL } class="w-6 h-6" alt={ provider.Name + " icon" } />
|
||||||
|
} else {
|
||||||
|
if provider.Type == db.ProviderTypeAuthentik {
|
||||||
|
<img src="/static/img/authentik.svg" class="w-6 h-6" alt="Authentik" />
|
||||||
|
} else if provider.Type == db.ProviderTypeOIDC {
|
||||||
|
<img src="/static/img/oidc.svg" class="w-6 h-6" alt="OIDC" />
|
||||||
|
} else if provider.Type == db.ProviderTypeSAML {
|
||||||
|
<img src="/static/img/saml.svg" class="w-6 h-6" alt="SAML" />
|
||||||
|
} else if provider.Type == db.ProviderTypeOAuth2 {
|
||||||
|
<img src="/static/img/oauth2.svg" class="w-6 h-6" alt="OAuth2" />
|
||||||
|
} else {
|
||||||
|
<i class="fas fa-user-shield text-blue-500 text-lg"></i>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 uppercase">
|
||||||
|
{ string(provider.Type) }
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
if provider.GetEnabled() {
|
||||||
|
<span class="px-2 py-1 text-xs rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
} else {
|
||||||
|
<span class="px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200">
|
||||||
|
Disabled
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 max-w-[200px] truncate">
|
||||||
|
{ provider.ProviderURL }
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
if provider.LastUsed.Valid {
|
||||||
|
{ formatTime(provider.LastUsed.Time) }
|
||||||
|
} else {
|
||||||
|
<span class="text-gray-400 dark:text-gray-500">Never</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<a href={ templ.SafeURL("/admin/settings/auth-providers/" + fmt.Sprint(provider.ID) + "/edit") }
|
||||||
|
class="text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 dark:focus:ring-gray-700"
|
||||||
|
title="Edit">
|
||||||
|
<i class="fas fa-edit w-3.5 h-3.5 mr-1.5"></i>
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-provider-id={ fmt.Sprint(provider.ID) }
|
||||||
|
data-provider-name={ provider.Name }
|
||||||
|
class="delete-provider-btn text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800"
|
||||||
|
title="Delete">
|
||||||
|
<i class="fas fa-trash-alt w-3.5 h-3.5 mr-1.5"></i>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-provider-id={ fmt.Sprint(provider.ID) }
|
||||||
|
onclick="testProvider(this)"
|
||||||
|
class="text-blue-700 bg-blue-100 hover:bg-blue-200 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-blue-700 dark:text-blue-300 dark:hover:bg-blue-600 dark:focus:ring-blue-800"
|
||||||
|
title="Test Connection">
|
||||||
|
<i class="fas fa-check-circle w-3.5 h-3.5 mr-1.5"></i>
|
||||||
|
Test
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 p-8 flex flex-col items-center justify-center text-center">
|
||||||
|
<div class="inline-flex h-16 w-16 flex-shrink-0 items-center justify-center rounded-full bg-gray-100 mb-4 dark:bg-gray-700">
|
||||||
|
<i class="fas fa-user-shield text-gray-400 dark:text-gray-500 text-3xl"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">No Authentication Providers</h3>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 mb-4">
|
||||||
|
You haven't set up any external authentication providers yet.
|
||||||
|
</p>
|
||||||
|
<a href="/admin/settings/auth-providers/new" class="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||||
|
Add First Provider
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Help Section -->
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg shadow-sm mt-8 p-4 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="flex items-start mb-2">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<i class="fas fa-info-circle w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 text-sm">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">Authentication providers allow users to sign in using external identity providers.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start mt-4">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<i class="fas fa-key w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 text-sm">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">Make sure to configure callback URLs in your provider's settings.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start mt-4">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<i class="fas fa-user-check w-4 h-4 text-indigo-500 dark:text-indigo-400 mr-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 text-sm">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">Test your connections to ensure proper communication with external authentication systems.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Provider Modal using Flowbite style -->
|
||||||
|
<div id="deleteProviderModal" tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
|
||||||
|
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
|
||||||
|
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||||
|
<div class="p-6 text-center">
|
||||||
|
<i class="fas fa-trash-alt text-red-400 text-3xl mb-4"></i>
|
||||||
|
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">Are you sure you want to delete the provider <strong id="providerNameConfirm"></strong>?</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="confirmDeleteBtn"
|
||||||
|
class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="closeDeleteModal()" class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Toast notification function
|
||||||
|
function showToast(type, message) {
|
||||||
|
const toastContainer = document.getElementById('toast-container');
|
||||||
|
|
||||||
|
// Create toast element
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.id = 'toast-' + type + '-' + Date.now();
|
||||||
|
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
||||||
|
toast.role = 'alert';
|
||||||
|
|
||||||
|
// Set toast content based on type
|
||||||
|
let iconClass, bgColorClass, textColorClass;
|
||||||
|
|
||||||
|
if (type === 'success') {
|
||||||
|
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
||||||
|
bgColorClass = 'text-green-500 dark:text-green-200';
|
||||||
|
textColorClass = 'text-green-500 dark:text-green-200';
|
||||||
|
} else if (type === 'error') {
|
||||||
|
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
||||||
|
bgColorClass = 'text-red-500 dark:text-red-200';
|
||||||
|
textColorClass = 'text-red-500 dark:text-red-200';
|
||||||
|
} else {
|
||||||
|
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
||||||
|
bgColorClass = 'text-blue-500 dark:text-blue-200';
|
||||||
|
textColorClass = 'text-blue-500 dark:text-blue-200';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set inner HTML with appropriate icon and message
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
||||||
|
${type === 'success'
|
||||||
|
? '<i class="fas fa-check"></i>'
|
||||||
|
: type === 'error'
|
||||||
|
? '<i class="fas fa-exclamation-circle"></i>'
|
||||||
|
: '<i class="fas fa-info-circle"></i>'}
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 text-sm font-normal">${message}</div>
|
||||||
|
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add toast to container
|
||||||
|
toastContainer.appendChild(toast);
|
||||||
|
|
||||||
|
// Trigger animation after a small delay to ensure the DOM has updated
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('translate-y-16', 'opacity-0');
|
||||||
|
toast.classList.add('translate-y-0', 'opacity-100');
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// Add event listener to close button
|
||||||
|
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
||||||
|
closeButton.addEventListener('click', function() {
|
||||||
|
// Animate out before removing
|
||||||
|
toast.classList.add('opacity-0', 'translate-y-4');
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-remove toast after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.add('opacity-0', 'translate-y-4');
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDeleteProvider(providerId, providerName) {
|
||||||
|
document.getElementById('providerNameConfirm').textContent = providerName;
|
||||||
|
document.getElementById('confirmDeleteBtn').onclick = () => deleteProvider(providerId);
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
|
document.getElementById('deleteProviderModal').classList.remove('hidden');
|
||||||
|
document.getElementById('deleteProviderModal').classList.add('flex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDeleteModal() {
|
||||||
|
document.getElementById('deleteProviderModal').classList.add('hidden');
|
||||||
|
document.getElementById('deleteProviderModal').classList.remove('flex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteProvider(providerId) {
|
||||||
|
fetch(`/admin/settings/auth-providers/${providerId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
showToast('error', data.error);
|
||||||
|
} else {
|
||||||
|
showToast('success', 'Authentication provider deleted successfully');
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
closeDeleteModal();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showToast('error', 'Failed to delete authentication provider');
|
||||||
|
closeDeleteModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testProvider(btn) {
|
||||||
|
const providerId = btn.getAttribute('data-provider-id');
|
||||||
|
const originalText = btn.innerHTML;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin w-3.5 h-3.5 mr-1.5"></i> Testing...';
|
||||||
|
|
||||||
|
fetch(`/admin/settings/auth-providers/${providerId}/test`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
showToast('error', data.error);
|
||||||
|
} else if (data.success) {
|
||||||
|
showToast('success', data.message || 'Connection test successful');
|
||||||
|
} else {
|
||||||
|
showToast('error', 'Connection test failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = originalText;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showToast('error', 'Connection test failed');
|
||||||
|
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = originalText;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set dark background color if in dark mode
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
if (document.documentElement.classList.contains('dark')) {
|
||||||
|
document.getElementById('auth-providers-container').style.backgroundColor = '#111827';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener for theme changes
|
||||||
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener('click', function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
document.getElementById('auth-providers-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up event listeners for delete buttons
|
||||||
|
document.querySelectorAll('.delete-provider-btn').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const providerId = this.getAttribute('data-provider-id');
|
||||||
|
const providerName = this.getAttribute('data-provider-name');
|
||||||
|
confirmDeleteProvider(providerId, providerName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/starfleetcptn/gomft/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getProviderIcon returns the appropriate icon for a provider
|
||||||
|
func getProviderIcon(provider db.AuthProvider) templ.Component {
|
||||||
|
// If provider has a custom icon URL, use it
|
||||||
|
if provider.IconURL != "" {
|
||||||
|
return templ.Raw(fmt.Sprintf(`<img src="%s" class="w-5 h-5" alt="%s icon" />`, provider.IconURL, provider.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise fall back to default icons based on type
|
||||||
|
switch provider.Type {
|
||||||
|
case db.ProviderTypeAuthentik:
|
||||||
|
return templ.Raw(`<img src="/static/img/authentik.svg" class="w-5 h-5" alt="Authentik" />`)
|
||||||
|
case db.ProviderTypeOIDC:
|
||||||
|
return templ.Raw(`<img src="/static/img/oidc.svg" class="w-5 h-5" alt="OIDC" />`)
|
||||||
|
case db.ProviderTypeSAML:
|
||||||
|
return templ.Raw(`<img src="/static/img/saml.svg" class="w-5 h-5" alt="SAML" />`)
|
||||||
|
case db.ProviderTypeOAuth2:
|
||||||
|
return templ.Raw(`<img src="/static/img/oauth2.svg" class="w-5 h-5" alt="OAuth2" />`)
|
||||||
|
default:
|
||||||
|
return templ.Raw(`<i class="fas fa-user-shield text-blue-500"></i>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ AuthProviderButtons(providers []db.AuthProvider) {
|
||||||
|
if len(providers) == 0 {
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-400 italic">
|
||||||
|
No external authentication providers available
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="space-y-2 w-full">
|
||||||
|
for _, provider := range providers {
|
||||||
|
if provider.GetEnabled() {
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL(fmt.Sprintf("/auth/provider/%d", provider.ID)) }
|
||||||
|
class="w-full inline-flex items-center justify-center px-4 py-2.5 bg-gray-100 border border-gray-300 rounded-lg font-medium text-gray-700 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-600"
|
||||||
|
>
|
||||||
|
<span class="flex-shrink-0 w-5 h-5 mr-2.5">
|
||||||
|
@getProviderIcon(provider)
|
||||||
|
</span>
|
||||||
|
<span>{ provider.Name }</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
+393
-167
@@ -7,12 +7,15 @@ import (
|
|||||||
"github.com/starfleetcptn/gomft/components/providers/source"
|
"github.com/starfleetcptn/gomft/components/providers/source"
|
||||||
"github.com/starfleetcptn/gomft/components/providers/destination"
|
"github.com/starfleetcptn/gomft/components/providers/destination"
|
||||||
"github.com/starfleetcptn/gomft/components/providers/common"
|
"github.com/starfleetcptn/gomft/components/providers/common"
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigFormData struct {
|
type ConfigFormData struct {
|
||||||
Config *db.TransferConfig
|
Config *db.TransferConfig
|
||||||
IsNew bool
|
IsNew bool
|
||||||
|
// Fields for pre-rendering flags on edit
|
||||||
|
InitialCommand *db.RcloneCommand
|
||||||
|
SelectedFlagsMap map[uint]bool
|
||||||
|
SelectedFlagValues map[uint]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConfigFormTitle(isNew bool) string {
|
func getConfigFormTitle(isNew bool) string {
|
||||||
@@ -28,7 +31,7 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
sourceType := "local"
|
sourceType := "local"
|
||||||
sourcePath := ""
|
sourcePath := ""
|
||||||
sourceHost := ""
|
sourceHost := ""
|
||||||
sourcePort := 22
|
sourcePort := 0 // Initialize to 0 to trigger default setting
|
||||||
sourceUser := ""
|
sourceUser := ""
|
||||||
sourcePassword := ""
|
sourcePassword := ""
|
||||||
sourceKeyFile := ""
|
sourceKeyFile := ""
|
||||||
@@ -47,7 +50,7 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
sourceTeamDrive := ""
|
sourceTeamDrive := ""
|
||||||
// Google Photos source fields
|
// Google Photos source fields
|
||||||
sourceReadOnly := false
|
sourceReadOnly := false
|
||||||
sourceStartYear, _ := strconv.Atoi(getCurrentYear())
|
sourceStartYear := 2025
|
||||||
sourceIncludeArchived := false
|
sourceIncludeArchived := false
|
||||||
|
|
||||||
filePattern := ""
|
filePattern := ""
|
||||||
@@ -56,7 +59,7 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
destinationType := "local"
|
destinationType := "local"
|
||||||
destinationPath := ""
|
destinationPath := ""
|
||||||
destHost := ""
|
destHost := ""
|
||||||
destPort := 22
|
destPort := 0 // Initialize to 0 to trigger default setting
|
||||||
destUser := ""
|
destUser := ""
|
||||||
destPassword := ""
|
destPassword := ""
|
||||||
destKeyFile := ""
|
destKeyFile := ""
|
||||||
@@ -75,7 +78,7 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
destTeamDrive := ""
|
destTeamDrive := ""
|
||||||
// Google Photos destination fields
|
// Google Photos destination fields
|
||||||
destReadOnly := false
|
destReadOnly := false
|
||||||
destStartYear, _ := strconv.Atoi(getCurrentYear()) // Default to current year int
|
destStartYear := 2025
|
||||||
destIncludeArchived := false
|
destIncludeArchived := false
|
||||||
|
|
||||||
archivePath := ""
|
archivePath := ""
|
||||||
@@ -84,6 +87,8 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
skipProcessedFiles := true
|
skipProcessedFiles := true
|
||||||
maxConcurrentTransfers := 4
|
maxConcurrentTransfers := 4
|
||||||
rcloneFlags := ""
|
rcloneFlags := ""
|
||||||
|
commandId := uint(1) // Default to 'copy' command
|
||||||
|
commandFlags := ""
|
||||||
useBuiltinAuthSource := true
|
useBuiltinAuthSource := true
|
||||||
useBuiltinAuthDest := true
|
useBuiltinAuthDest := true
|
||||||
|
|
||||||
@@ -117,7 +122,7 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
if config.SourceReadOnly != nil {
|
if config.SourceReadOnly != nil {
|
||||||
sourceReadOnly = *config.SourceReadOnly
|
sourceReadOnly = *config.SourceReadOnly
|
||||||
}
|
}
|
||||||
sourceStartYear = config.SourceStartYear
|
// sourceStartYear = config.SourceStartYear
|
||||||
if config.SourceIncludeArchived != nil {
|
if config.SourceIncludeArchived != nil {
|
||||||
sourceIncludeArchived = *config.SourceIncludeArchived
|
sourceIncludeArchived = *config.SourceIncludeArchived
|
||||||
}
|
}
|
||||||
@@ -152,7 +157,7 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
if config.DestReadOnly != nil {
|
if config.DestReadOnly != nil {
|
||||||
destReadOnly = *config.DestReadOnly
|
destReadOnly = *config.DestReadOnly
|
||||||
}
|
}
|
||||||
destStartYear = config.DestStartYear
|
// destStartYear = config.DestStartYear
|
||||||
if config.DestIncludeArchived != nil {
|
if config.DestIncludeArchived != nil {
|
||||||
destIncludeArchived = *config.DestIncludeArchived
|
destIncludeArchived = *config.DestIncludeArchived
|
||||||
}
|
}
|
||||||
@@ -163,6 +168,8 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
skipProcessedFiles = config.GetSkipProcessedFiles()
|
skipProcessedFiles = config.GetSkipProcessedFiles()
|
||||||
maxConcurrentTransfers = config.MaxConcurrentTransfers
|
maxConcurrentTransfers = config.MaxConcurrentTransfers
|
||||||
rcloneFlags = config.RcloneFlags
|
rcloneFlags = config.RcloneFlags
|
||||||
|
commandId = config.CommandID
|
||||||
|
commandFlags = config.CommandFlags
|
||||||
if config.UseBuiltinAuthSource != nil {
|
if config.UseBuiltinAuthSource != nil {
|
||||||
useBuiltinAuthSource = *config.UseBuiltinAuthSource
|
useBuiltinAuthSource = *config.UseBuiltinAuthSource
|
||||||
} else if sourceClientId != "" || sourceClientSecret != "" {
|
} else if sourceClientId != "" || sourceClientSecret != "" {
|
||||||
@@ -238,12 +245,17 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
skipProcessedFiles: %v,
|
skipProcessedFiles: %v,
|
||||||
maxConcurrentTransfers: %d,
|
maxConcurrentTransfers: %d,
|
||||||
rcloneFlags: '%s',
|
rcloneFlags: '%s',
|
||||||
|
commandId: %d,
|
||||||
|
commandFlags: '%s',
|
||||||
|
|
||||||
// Path validation states
|
// Path validation states
|
||||||
sourcePathValid: null,
|
sourcePathValid: null,
|
||||||
sourcePathError: '',
|
sourcePathError: '',
|
||||||
destPathValid: null,
|
destPathValid: null,
|
||||||
destPathError: '',
|
destPathError: '',
|
||||||
|
|
||||||
|
// Command configuration
|
||||||
|
requiresDestination: true,
|
||||||
|
|
||||||
// Methods for path validation
|
// Methods for path validation
|
||||||
checkPath(path, type) {
|
checkPath(path, type) {
|
||||||
@@ -263,6 +275,69 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
this[type + 'PathValid'] = false;
|
this[type + 'PathValid'] = false;
|
||||||
this[type + 'PathError'] = 'Error checking path: ' + error.message;
|
this[type + 'PathError'] = 'Error checking path: ' + error.message;
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Method to check if destination is required based on command type
|
||||||
|
updateCommandRequirements() {
|
||||||
|
// List of commands that don't require destination
|
||||||
|
const listingCommands = ['ls', 'lsd', 'lsl', 'lsf', 'lsjson', 'listremotes'];
|
||||||
|
const infoCommands = ['md5sum', 'sha1sum', 'size', 'version'];
|
||||||
|
const dirCommands = ['mkdir', 'rmdir', 'rmdirs'];
|
||||||
|
const destructiveCommands = ['delete', 'purge'];
|
||||||
|
const specialSinglePathCommands = ['obscure'];
|
||||||
|
|
||||||
|
// Get the command name from the command ID
|
||||||
|
// This will need coordination with your backend to ensure the IDs match the commands
|
||||||
|
let commandName = '';
|
||||||
|
switch(parseInt(this.commandId)) {
|
||||||
|
// Correct mapping based on internal/db/migrations/009_add_rclone_tables.go
|
||||||
|
case 1: commandName = 'copy'; break;
|
||||||
|
case 2: commandName = 'sync'; break;
|
||||||
|
case 3: commandName = 'bisync'; break;
|
||||||
|
case 4: commandName = 'move'; break;
|
||||||
|
case 5: commandName = 'delete'; break;
|
||||||
|
case 6: commandName = 'purge'; break;
|
||||||
|
case 7: commandName = 'mkdir'; break;
|
||||||
|
case 8: commandName = 'rmdir'; break;
|
||||||
|
case 9: commandName = 'rmdirs'; break;
|
||||||
|
case 10: commandName = 'check'; break;
|
||||||
|
case 11: commandName = 'ls'; break;
|
||||||
|
case 12: commandName = 'lsd'; break;
|
||||||
|
case 13: commandName = 'lsl'; break;
|
||||||
|
case 14: commandName = 'lsf'; break;
|
||||||
|
case 15: commandName = 'lsjson'; break;
|
||||||
|
case 16: commandName = 'md5sum'; break;
|
||||||
|
case 17: commandName = 'sha1sum'; break;
|
||||||
|
case 18: commandName = 'size'; break;
|
||||||
|
case 19: commandName = 'version'; break;
|
||||||
|
case 20: commandName = 'cleanup'; break;
|
||||||
|
case 21: commandName = 'dedupe'; break;
|
||||||
|
case 22: commandName = 'copyto'; break;
|
||||||
|
case 23: commandName = 'moveto'; break;
|
||||||
|
case 24: commandName = 'listremotes'; break;
|
||||||
|
case 25: commandName = 'obscure'; break;
|
||||||
|
case 26: commandName = 'cryptcheck'; break;
|
||||||
|
default: commandName = 'copy'; // Default to copy if ID is unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Command ID:', this.commandId, 'Command Name:', commandName);
|
||||||
|
|
||||||
|
// Check if the command requires a destination
|
||||||
|
if (
|
||||||
|
listingCommands.includes(commandName) ||
|
||||||
|
infoCommands.includes(commandName) ||
|
||||||
|
(dirCommands.includes(commandName) && !this.rcloneFlags.includes('--dst')) ||
|
||||||
|
destructiveCommands.includes(commandName) ||
|
||||||
|
specialSinglePathCommands.includes(commandName) ||
|
||||||
|
commandName === 'version' ||
|
||||||
|
commandName === 'listremotes'
|
||||||
|
) {
|
||||||
|
this.requiresDestination = false;
|
||||||
|
console.log('Destination not required for command:', commandName);
|
||||||
|
} else {
|
||||||
|
this.requiresDestination = true;
|
||||||
|
console.log('Destination required for command:', commandName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
name, sourceType, sourcePath, sourceHost, sourcePort, sourceUser, sourcePassword, sourceKeyFile, sourceAuthType,
|
name, sourceType, sourcePath, sourceHost, sourcePort, sourceUser, sourcePassword, sourceKeyFile, sourceAuthType,
|
||||||
@@ -275,171 +350,322 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
destClientId, destClientSecret, destDriveId, destTeamDrive,
|
destClientId, destClientSecret, destDriveId, destTeamDrive,
|
||||||
destReadOnly, destStartYear, destIncludeArchived,
|
destReadOnly, destStartYear, destIncludeArchived,
|
||||||
useBuiltinAuthSource, useBuiltinAuthDest,
|
useBuiltinAuthSource, useBuiltinAuthDest,
|
||||||
archivePath, archiveEnabled, deleteAfterTransfer, skipProcessedFiles, maxConcurrentTransfers, rcloneFlags)
|
archivePath, archiveEnabled, deleteAfterTransfer, skipProcessedFiles, maxConcurrentTransfers, rcloneFlags,
|
||||||
|
commandId, commandFlags)
|
||||||
}
|
}
|
||||||
|
|
||||||
templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
||||||
@LayoutWithContext(getConfigFormTitle(data.IsNew), ctx) {
|
@LayoutWithContext(getConfigFormTitle(data.IsNew), ctx) {
|
||||||
<div class="flex flex-col items-center justify-center min-h-screen bg-secondary-50 dark:bg-secondary-900 py-12 px-4 sm:px-6 lg:px-8">
|
<!-- Main Content -->
|
||||||
<div class="max-w-3xl w-full bg-white dark:bg-secondary-800 rounded-lg shadow-md p-8 border border-secondary-200 dark:border-secondary-700">
|
<section class="py-8 px-4">
|
||||||
<div class="flex justify-center mb-6">
|
<div class="mx-auto max-w-3xl">
|
||||||
<div class="p-4 bg-primary-100 dark:bg-primary-900 rounded-full">
|
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-6 md:p-8">
|
||||||
<i class="fas fa-cog text-primary-500 dark:text-primary-400 text-2xl"></i>
|
<!-- Form Header -->
|
||||||
|
<div class="mb-8 text-center">
|
||||||
|
<div class="flex justify-center mb-4">
|
||||||
|
<span class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-50 dark:bg-blue-900">
|
||||||
|
<i class="fas fa-cog text-blue-600 dark:text-blue-300 text-2xl"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{ getConfigFormTitle(data.IsNew) }
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Configure your file transfer settings using the form below
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Form -->
|
||||||
|
<form
|
||||||
|
class="space-y-6"
|
||||||
|
if data.IsNew {
|
||||||
|
hx-post="/configs"
|
||||||
|
} else {
|
||||||
|
hx-post={ fmt.Sprintf("/configs/%d", data.Config.ID) }
|
||||||
|
}
|
||||||
|
hx-target="body"
|
||||||
|
hx-redirect="/configs"
|
||||||
|
hx-boost="true"
|
||||||
|
x-data={ getInitialData(data.Config) }
|
||||||
|
x-init="$nextTick(() => {
|
||||||
|
// Ensure initial form state displays correctly on load
|
||||||
|
sourceType = sourceType || 'local';
|
||||||
|
destinationType = destinationType || 'local';
|
||||||
|
|
||||||
|
// Set default ports based on connection type
|
||||||
|
if (sourcePort === 0 || !sourcePort) {
|
||||||
|
if (sourceType === 'sftp') {
|
||||||
|
sourcePort = 22;
|
||||||
|
} else if (sourceType === 'ftp') {
|
||||||
|
sourcePort = 21;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destPort === 0 || !destPort) {
|
||||||
|
if (destinationType === 'sftp') {
|
||||||
|
destPort = 22;
|
||||||
|
} else if (destinationType === 'ftp') {
|
||||||
|
destPort = 21;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize command requirements
|
||||||
|
updateCommandRequirements();
|
||||||
|
})"
|
||||||
|
x-effect="if (sourceType === 'sftp' && (sourcePort === 0 || sourcePort === 21)) {
|
||||||
|
sourcePort = 22;
|
||||||
|
console.log('Updating source port to 22 for SFTP');
|
||||||
|
} else if (sourceType === 'ftp' && (sourcePort === 0 || sourcePort === 22)) {
|
||||||
|
sourcePort = 21;
|
||||||
|
console.log('Updating source port to 21 for FTP');
|
||||||
|
}"
|
||||||
|
x-effect="if (destinationType === 'sftp' && (destPort === 0 || destPort === 21)) {
|
||||||
|
destPort = 22;
|
||||||
|
console.log('Updating destination port to 22 for SFTP');
|
||||||
|
} else if (destinationType === 'ftp' && (destPort === 0 || destPort === 22)) {
|
||||||
|
destPort = 21;
|
||||||
|
console.log('Updating destination port to 21 for FTP');
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- Configuration Details Section -->
|
||||||
|
<div class="p-4 mb-4 bg-blue-50 border border-blue-100 rounded-lg dark:bg-blue-900/20 dark:border-blue-900">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<i class="fas fa-info-circle text-blue-600 dark:text-blue-400 mr-2"></i>
|
||||||
|
<h3 class="text-lg font-medium text-blue-600 dark:text-blue-400">Configuration Details</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
Give your configuration a descriptive name and set up the source and destination locations.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Name field -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Configuration Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
x-model="name"
|
||||||
|
required
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="My Transfer Configuration"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Choose a descriptive name to identify this configuration.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rclone Command Configuration Section -->
|
||||||
|
<div class="p-5 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<h3 class="mb-4 text-xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-terminal mr-2 text-blue-500 dark:text-blue-400"></i>Command Configuration
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Additonal Rclone Flags -->
|
||||||
|
@common.RcloneFlags(data.Config.CommandID) // Pass current command ID
|
||||||
|
|
||||||
|
<!-- Container for flags, pre-rendered on edit, loaded via HTMX on new/change -->
|
||||||
|
<div id="command-flags-container" class="mt-4">
|
||||||
|
if !data.IsNew && data.InitialCommand != nil {
|
||||||
|
// Pre-render flags if editing and command data is available
|
||||||
|
@common.RcloneCommandFlagsContent(data.InitialCommand, data.SelectedFlagsMap, data.SelectedFlagValues)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Source Configuration Section -->
|
||||||
|
<div class="p-5 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<h3 class="mb-4 text-xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-upload mr-2 text-blue-500 dark:text-blue-400"></i>Source Configuration
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Source selection -->
|
||||||
|
@common.SourceSelection()
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<button type="button"
|
||||||
|
class="text-white bg-green-600 hover:bg-green-700 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-4 py-2 text-center dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800"
|
||||||
|
hx-post="/configs/test-connection"
|
||||||
|
hx-include="closest form"
|
||||||
|
hx-vals='{"providerType": "source"}'
|
||||||
|
hx-swap="none"
|
||||||
|
hx-indicator="#source-test-spinner">
|
||||||
|
<i class="fas fa-plug mr-1"></i> Test Source
|
||||||
|
<span id="source-test-spinner" class="htmx-indicator ml-2"><i class="fas fa-spinner fa-spin"></i></span>
|
||||||
|
</button>
|
||||||
|
<!-- Removed target div, result shown via toast -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Source type specific forms -->
|
||||||
|
<template x-if="sourceType === 'local'">
|
||||||
|
@source.LocalSourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'sftp'">
|
||||||
|
@source.SFTPSourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'ftp'">
|
||||||
|
@source.FTPSourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 's3'">
|
||||||
|
@source.S3SourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'minio'">
|
||||||
|
@source.MinIOSourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'smb'">
|
||||||
|
@source.SMBSourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'webdav'">
|
||||||
|
@source.WebDAVSourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'nextcloud'">
|
||||||
|
@source.NextCloudSourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'gdrive'">
|
||||||
|
@source.GoogleDriveSourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'gphotos'">
|
||||||
|
@source.GooglePhotosSourceForm()
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- File Pattern Section -->
|
||||||
|
<div class="p-5 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<h3 class="mb-4 text-xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-filter mr-2 text-blue-500 dark:text-blue-400"></i>File Patterns
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- File pattern fields -->
|
||||||
|
@common.FilePatternFields()
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Destination Configuration Section (only shown if required) -->
|
||||||
|
<div x-show="requiresDestination" x-transition class="p-5 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<h3 class="mb-4 text-xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-download mr-2 text-blue-500 dark:text-blue-400"></i>Destination Configuration
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Destination selection -->
|
||||||
|
@common.DestinationSelection()
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<button type="button"
|
||||||
|
class="text-white bg-green-600 hover:bg-green-700 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-4 py-2 text-center dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800"
|
||||||
|
hx-post="/configs/test-connection"
|
||||||
|
hx-include="closest form"
|
||||||
|
hx-vals='{"providerType": "destination"}'
|
||||||
|
hx-swap="none"
|
||||||
|
hx-indicator="#dest-test-spinner">
|
||||||
|
<i class="fas fa-plug mr-1"></i> Test Destination
|
||||||
|
<span id="dest-test-spinner" class="htmx-indicator ml-2"><i class="fas fa-spinner fa-spin"></i></span>
|
||||||
|
</button>
|
||||||
|
<!-- Removed target div, result shown via toast -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Destination type specific forms -->
|
||||||
|
<template x-if="destinationType === 'local'">
|
||||||
|
@destination.LocalDestinationForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="destinationType === 'sftp'">
|
||||||
|
@destination.SFTPDestinationForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="destinationType === 'ftp'">
|
||||||
|
@destination.FTPDestinationForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="destinationType === 's3'">
|
||||||
|
@destination.S3DestinationForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="destinationType === 'minio'">
|
||||||
|
@destination.MinIODestinationForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="destinationType === 'smb'">
|
||||||
|
@destination.SMBDestinationForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="destinationType === 'nextcloud'">
|
||||||
|
@destination.NextCloudDestinationForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="destinationType === 'webdav'">
|
||||||
|
@destination.WebDAVDestinationForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="destinationType === 'gdrive'">
|
||||||
|
@destination.GoogleDriveDestinationForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="destinationType === 'gphotos'">
|
||||||
|
@destination.GooglePhotosDestinationForm()
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Advanced Options Section -->
|
||||||
|
<div class="p-5 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<h3 class="mb-4 text-xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-cogs mr-2 text-blue-500 dark:text-blue-400"></i>Advanced Options
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Archive options -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h4 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Archive Settings</h4>
|
||||||
|
@common.ArchiveOptions()
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form Actions -->
|
||||||
|
<div class="flex items-center justify-between pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<a href="/configs"
|
||||||
|
class="text-white bg-gray-500 hover:bg-gray-600 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800">
|
||||||
|
<i class="fas fa-arrow-left mr-2"></i>Cancel
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
|
<span class="flex items-center">
|
||||||
|
<i class="fas fa-save mr-2"></i>
|
||||||
|
if data.IsNew {
|
||||||
|
Create Configuration
|
||||||
|
} else {
|
||||||
|
Save Changes
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Help Card -->
|
||||||
|
<div class="mt-4 p-4 bg-gray-50 border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<i class="fas fa-lightbulb text-yellow-400 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<h5 class="text-sm font-medium text-gray-900 dark:text-white">Configuration Tips</h5>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Configure your file transfer settings carefully for optimal performance. Use file patterns to filter which files are transferred, and consider using archive options to keep track of processed files.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
<h1 class="mt-2 text-center text-3xl font-extrabold text-secondary-900 dark:text-secondary-100">
|
|
||||||
{ getConfigFormTitle(data.IsNew) }
|
|
||||||
</h1>
|
|
||||||
<p class="mt-2 text-center text-sm text-secondary-600 dark:text-secondary-400">
|
|
||||||
Set up your file transfer configuration
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form
|
|
||||||
class="space-y-8"
|
|
||||||
if data.IsNew {
|
|
||||||
hx-post="/configs"
|
|
||||||
} else {
|
|
||||||
hx-post={ fmt.Sprintf("/configs/%d", data.Config.ID) }
|
|
||||||
}
|
|
||||||
hx-target="body"
|
|
||||||
hx-redirect="/configs"
|
|
||||||
hx-boost="true"
|
|
||||||
x-data={ getInitialData(data.Config) }
|
|
||||||
x-init="$nextTick(() => {
|
|
||||||
// Ensure initial form state displays correctly on load
|
|
||||||
sourceType = sourceType || 'local';
|
|
||||||
destinationType = destinationType || 'local';
|
|
||||||
sourcePort = sourcePort || 22;
|
|
||||||
destPort = destPort || 22;
|
|
||||||
})"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- Name field -->
|
|
||||||
@common.NameField()
|
|
||||||
|
|
||||||
<!-- Source section -->
|
|
||||||
@common.SourceSelection()
|
|
||||||
|
|
||||||
<!-- Source type specific forms -->
|
|
||||||
<template x-if="sourceType === 'local'">
|
|
||||||
@source.LocalSourceForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="sourceType === 'sftp'">
|
|
||||||
@source.SFTPSourceForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="sourceType === 'ftp'">
|
|
||||||
@source.FTPSourceForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="sourceType === 's3'">
|
|
||||||
@source.S3SourceForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="sourceType === 'minio'">
|
|
||||||
@source.MinIOSourceForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="sourceType === 'smb'">
|
|
||||||
@source.SMBSourceForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="sourceType === 'webdav'">
|
|
||||||
@source.WebDAVSourceForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="sourceType === 'nextcloud'">
|
|
||||||
@source.NextCloudSourceForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="sourceType === 'gdrive'">
|
|
||||||
@source.GoogleDriveSourceForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="sourceType === 'gphotos'">
|
|
||||||
@source.GooglePhotosSourceForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- File pattern fields -->
|
|
||||||
@common.FilePatternFields()
|
|
||||||
|
|
||||||
<!-- Destination section -->
|
|
||||||
@common.DestinationSelection()
|
|
||||||
|
|
||||||
<!-- Destination type specific forms -->
|
|
||||||
<template x-if="destinationType === 'local'">
|
|
||||||
@destination.LocalDestinationForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="destinationType === 'sftp'">
|
|
||||||
@destination.SFTPDestinationForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="destinationType === 'ftp'">
|
|
||||||
@destination.FTPDestinationForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="destinationType === 's3'">
|
|
||||||
@destination.S3DestinationForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="destinationType === 'minio'">
|
|
||||||
@destination.MinIODestinationForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="destinationType === 'smb'">
|
|
||||||
@destination.SMBDestinationForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="destinationType === 'nextcloud'">
|
|
||||||
@destination.NextCloudDestinationForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="destinationType === 'webdav'">
|
|
||||||
@destination.WebDAVDestinationForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="destinationType === 'gdrive'">
|
|
||||||
@destination.GoogleDriveDestinationForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="destinationType === 'gphotos'">
|
|
||||||
@destination.GooglePhotosDestinationForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Archive options -->
|
|
||||||
@common.ArchiveOptions()
|
|
||||||
|
|
||||||
<!-- Rclone flags -->
|
|
||||||
@common.RcloneFlags()
|
|
||||||
|
|
||||||
<!-- Form actions -->
|
|
||||||
<div class="flex justify-end mt-8">
|
|
||||||
<a href="/configs"
|
|
||||||
class="btn-secondary mr-4">
|
|
||||||
Cancel
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn-primary">
|
|
||||||
<span class="flex items-center">
|
|
||||||
<i class="fas fa-save mr-2"></i>
|
|
||||||
if data.IsNew {
|
|
||||||
Create Configuration
|
|
||||||
} else {
|
|
||||||
Save Changes
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-6 text-center text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
<p>Configure your file transfer settings carefully for optimal performance</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+289
-122
@@ -6,55 +6,68 @@ import (
|
|||||||
"github.com/starfleetcptn/gomft/internal/db"
|
"github.com/starfleetcptn/gomft/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Dialog component for confirmation dialogs
|
// Dialog component for confirmation dialogs using Flowbite modal
|
||||||
templ ConfigDialog(id string, title string, message string, confirmClass string, confirmText string, action string, configID uint, configName string) {
|
templ ConfigDialog(id string, title string, message string, confirmClass string, confirmText string, action string, configID uint, configName string) {
|
||||||
<div id={ id } class="hidden fixed inset-0 bg-secondary-900/50 dark:bg-secondary-900/80 backdrop-blur-sm z-50 flex items-center justify-center">
|
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
||||||
<div class="bg-white dark:bg-secondary-800 rounded-lg shadow-xl max-w-md w-full mx-4 overflow-hidden">
|
<!-- Backdrop -->
|
||||||
<div class="px-6 pt-5 pb-3 text-center">
|
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
|
||||||
<div class="flex justify-center mb-2">
|
<!-- Modal content -->
|
||||||
<i class="fas fa-exclamation-triangle text-yellow-400 text-3xl"></i>
|
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
|
||||||
|
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||||
|
<div class="p-6 text-center">
|
||||||
|
if action == "delete" {
|
||||||
|
<i class="fas fa-trash-alt text-red-400 text-3xl mb-4"></i>
|
||||||
|
} else {
|
||||||
|
<i class="fas fa-exclamation-circle text-yellow-400 text-3xl mb-4"></i>
|
||||||
|
}
|
||||||
|
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{ message }</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={ confirmClass }
|
||||||
|
hx-delete={ fmt.Sprintf("/configs/%d", configID) }
|
||||||
|
hx-target="closest li"
|
||||||
|
hx-swap="delete"
|
||||||
|
data-config-name={ configName }
|
||||||
|
data-config-id={ fmt.Sprint(configID) }
|
||||||
|
id={ fmt.Sprintf("delete-config-btn-%d", configID) }
|
||||||
|
onclick={ triggerConfigDelete(id, configID, configName) }>
|
||||||
|
{ confirmText }
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick={ closeModal(id) } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-medium text-secondary-900 dark:text-secondary-100">
|
|
||||||
{ title }
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="px-6 py-4 text-center">
|
|
||||||
<p class="text-secondary-700 dark:text-secondary-300">
|
|
||||||
{ message }
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="px-6 py-4 flex justify-end space-x-3">
|
|
||||||
<button type="button" class="btn-secondary" onclick={ hideConfigDialog(id) }>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={ confirmClass }
|
|
||||||
hx-delete={ fmt.Sprintf("/configs/%d", configID) }
|
|
||||||
hx-target="closest li"
|
|
||||||
hx-swap="delete"
|
|
||||||
data-config-name={ configName }
|
|
||||||
data-config-id={ fmt.Sprint(configID) }
|
|
||||||
id={ fmt.Sprintf("delete-config-btn-%d", configID) }
|
|
||||||
onclick={ triggerConfigDelete(id, configID, configName) }>
|
|
||||||
{ confirmText }
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
script hideConfigDialog(id string) {
|
script closeModal(id string) {
|
||||||
document.getElementById(id).classList.add("hidden");
|
const modal = document.getElementById(id);
|
||||||
|
const backdrop = document.getElementById(id + '-backdrop');
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
}
|
||||||
|
if (backdrop) {
|
||||||
|
backdrop.remove();
|
||||||
|
}
|
||||||
|
document.body.style.overflow = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
script showConfigDialog(id string) {
|
script showModal(id string) {
|
||||||
document.getElementById(id).classList.remove("hidden");
|
const modal = document.getElementById(id);
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
modal.classList.add('flex');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
script triggerConfigDelete(dialogId string, configID uint, configName string) {
|
script triggerConfigDelete(dialogId string, configID uint, configName string) {
|
||||||
// Hide the dialog
|
// Hide the dialog
|
||||||
document.getElementById(dialogId).classList.add("hidden");
|
document.getElementById(dialogId).classList.add("hidden");
|
||||||
|
document.getElementById(dialogId).classList.remove("flex");
|
||||||
|
|
||||||
// Store data in a way that's accessible to event handlers
|
// Store data in a way that's accessible to event handlers
|
||||||
window.lastDeletedConfig = {
|
window.lastDeletedConfig = {
|
||||||
@@ -75,38 +88,82 @@ type ConfigsData struct {
|
|||||||
|
|
||||||
templ Configs(ctx context.Context, data ConfigsData) {
|
templ Configs(ctx context.Context, data ConfigsData) {
|
||||||
@LayoutWithContext("Transfer Configurations", ctx) {
|
@LayoutWithContext("Transfer Configurations", ctx) {
|
||||||
|
<!-- Status and Error Messages -->
|
||||||
|
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Debug notification system
|
// Debug notification system
|
||||||
console.log("Configs template loaded, setting up notification system");
|
console.log("Configs template loaded, setting up notification system");
|
||||||
|
|
||||||
// Create a global notyf instance if it doesn't exist yet
|
// Function to create and show a toast
|
||||||
if (!window.notyf) {
|
function showToast(message, type) {
|
||||||
window.notyf = new Notyf({
|
const toastContainer = document.getElementById('toast-container');
|
||||||
duration: 3000,
|
|
||||||
position: {
|
// Create toast element
|
||||||
x: 'right',
|
const toast = document.createElement('div');
|
||||||
y: 'top',
|
toast.id = 'toast-' + type + '-' + Date.now();
|
||||||
},
|
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
||||||
types: [
|
toast.role = 'alert';
|
||||||
{
|
|
||||||
type: 'success',
|
// Set toast content based on type
|
||||||
background: '#38c172',
|
let iconClass, bgColorClass, textColorClass;
|
||||||
icon: {
|
|
||||||
className: 'fas fa-check-circle',
|
if (type === 'success') {
|
||||||
tagName: 'i'
|
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
||||||
}
|
bgColorClass = 'text-green-500 dark:text-green-200';
|
||||||
},
|
textColorClass = 'text-green-500 dark:text-green-200';
|
||||||
{
|
} else if (type === 'error') {
|
||||||
type: 'error',
|
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
||||||
background: '#e3342f',
|
bgColorClass = 'text-red-500 dark:text-red-200';
|
||||||
icon: {
|
textColorClass = 'text-red-500 dark:text-red-200';
|
||||||
className: 'fas fa-exclamation-circle',
|
} else {
|
||||||
tagName: 'i'
|
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
||||||
}
|
bgColorClass = 'text-blue-500 dark:text-blue-200';
|
||||||
}
|
textColorClass = 'text-blue-500 dark:text-blue-200';
|
||||||
]
|
}
|
||||||
|
|
||||||
|
// Set inner HTML with appropriate icon and message
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
||||||
|
${type === 'success'
|
||||||
|
? '<i class="fas fa-check"></i>'
|
||||||
|
: type === 'error'
|
||||||
|
? '<i class="fas fa-exclamation-circle"></i>'
|
||||||
|
: '<i class="fas fa-info-circle"></i>'}
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 text-sm font-normal">${message}</div>
|
||||||
|
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add toast to container
|
||||||
|
toastContainer.appendChild(toast);
|
||||||
|
|
||||||
|
// Trigger animation after a small delay to ensure the DOM has updated
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('translate-y-16', 'opacity-0');
|
||||||
|
toast.classList.add('translate-y-0', 'opacity-100');
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// Add event listener to close button
|
||||||
|
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
||||||
|
closeButton.addEventListener('click', function() {
|
||||||
|
// Animate out before removing
|
||||||
|
toast.classList.add('opacity-0', 'translate-y-4');
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 300);
|
||||||
});
|
});
|
||||||
console.log("Notyf initialized:", window.notyf);
|
|
||||||
|
// Auto-remove toast after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.add('opacity-0', 'translate-y-4');
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show status messages based on URL parameters
|
// Show status messages based on URL parameters
|
||||||
@@ -122,12 +179,23 @@ templ Configs(ctx context.Context, data ConfigsData) {
|
|||||||
if (errorDetails) {
|
if (errorDetails) {
|
||||||
message += ": " + errorDetails;
|
message += ": " + errorDetails;
|
||||||
}
|
}
|
||||||
window.notyf.error(message);
|
showToast(message, 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'gdrive_auth_success') {
|
if (status === 'gdrive_auth_success') {
|
||||||
window.notyf.success("Google Drive authentication completed successfully");
|
showToast("Google Drive authentication completed successfully", 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle modal hide buttons
|
||||||
|
const hideButtons = document.querySelectorAll('[data-modal-hide]');
|
||||||
|
hideButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const modalId = this.getAttribute('data-modal-hide');
|
||||||
|
const modal = document.getElementById(modalId);
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track all HTMX events for debugging
|
// Track all HTMX events for debugging
|
||||||
@@ -143,6 +211,18 @@ templ Configs(ctx context.Context, data ConfigsData) {
|
|||||||
// This is definitely a delete request - store this information
|
// This is definitely a delete request - store this information
|
||||||
window.isConfigDeleteRequest = true;
|
window.isConfigDeleteRequest = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is a duplication request
|
||||||
|
if (path && method === 'POST' && path.match(/^\/configs\/\d+\/duplicate$/)) {
|
||||||
|
window.isConfigDuplicateRequest = true;
|
||||||
|
|
||||||
|
// Store the config ID for reference
|
||||||
|
const configId = path.match(/^\/configs\/(\d+)\/duplicate$/)[1];
|
||||||
|
const configButton = document.querySelector(`button[hx-post="/configs/${configId}/duplicate"]`);
|
||||||
|
if (configButton) {
|
||||||
|
window.duplicatingConfigName = configButton.closest('li').querySelector('.text-blue-600').textContent.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track HTMX after-request events for config deletion
|
// Track HTMX after-request events for config deletion
|
||||||
@@ -175,13 +255,29 @@ templ Configs(ctx context.Context, data ConfigsData) {
|
|||||||
configName = window.lastDeletedConfig.name;
|
configName = window.lastDeletedConfig.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.notyf.success(`Configuration "${configName}" deleted successfully`);
|
showToast(`Configuration "${configName}" deleted successfully`, 'success');
|
||||||
|
|
||||||
// Clear flags
|
// Clear flags
|
||||||
window.currentlyDeletingConfig = false;
|
window.currentlyDeletingConfig = false;
|
||||||
window.isConfigDeleteRequest = false;
|
window.isConfigDeleteRequest = false;
|
||||||
window.lastDeletedConfig = null;
|
window.lastDeletedConfig = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is a duplication request
|
||||||
|
const isDuplicateRequest = window.isConfigDuplicateRequest &&
|
||||||
|
event.detail.pathInfo &&
|
||||||
|
event.detail.pathInfo.requestPath &&
|
||||||
|
event.detail.pathInfo.requestPath.match(/^\/configs\/\d+\/duplicate$/);
|
||||||
|
|
||||||
|
// If this is a successful duplication request, show notification
|
||||||
|
if (isDuplicateRequest && event.detail.successful) {
|
||||||
|
const configName = window.duplicatingConfigName || "configuration";
|
||||||
|
showToast(`Configuration "${configName}" duplicated successfully`, 'success');
|
||||||
|
|
||||||
|
// Clear flags
|
||||||
|
window.isConfigDuplicateRequest = false;
|
||||||
|
window.duplicatingConfigName = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track HTMX error events for config deletion
|
// Track HTMX error events for config deletion
|
||||||
@@ -218,69 +314,97 @@ templ Configs(ctx context.Context, data ConfigsData) {
|
|||||||
errorMsg = `Error: ${error.error}`;
|
errorMsg = `Error: ${error.error}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.notyf.error(errorMsg);
|
showToast(errorMsg, 'error');
|
||||||
|
|
||||||
// Clear flags
|
// Clear flags
|
||||||
window.currentlyDeletingConfig = false;
|
window.currentlyDeletingConfig = false;
|
||||||
window.isConfigDeleteRequest = false;
|
window.isConfigDeleteRequest = false;
|
||||||
window.lastDeletedConfig = null;
|
window.lastDeletedConfig = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is a duplication request
|
||||||
|
const isDuplicateRequest = window.isConfigDuplicateRequest &&
|
||||||
|
event.detail.pathInfo &&
|
||||||
|
event.detail.pathInfo.requestPath &&
|
||||||
|
event.detail.pathInfo.requestPath.match(/^\/configs\/\d+\/duplicate$/);
|
||||||
|
|
||||||
|
if (isDuplicateRequest) {
|
||||||
|
const configName = window.duplicatingConfigName || "configuration";
|
||||||
|
|
||||||
|
let errorMsg = `Failed to duplicate configuration "${configName}"`;
|
||||||
|
|
||||||
|
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||||
|
try {
|
||||||
|
const error = JSON.parse(event.detail.xhr.responseText);
|
||||||
|
errorMsg = `Error: ${error.error}`;
|
||||||
|
} catch (e) {
|
||||||
|
// If not JSON, use the response text directly
|
||||||
|
if (event.detail.xhr.responseText.trim()) {
|
||||||
|
errorMsg = event.detail.xhr.responseText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(errorMsg, 'error');
|
||||||
|
|
||||||
|
// Clear flags
|
||||||
|
window.isConfigDuplicateRequest = false;
|
||||||
|
window.duplicatingConfigName = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="py-6">
|
<div id="configs-container" style="min-height: 100vh; background-color: rgb(249, 250, 251);" class="configs-page bg-gray-50 dark:bg-gray-900">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="pb-8 w-full">
|
||||||
<div class="flex justify-between items-center mb-8">
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
<h1 class="text-3xl font-bold text-secondary-900 dark:text-secondary-100">
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
<i class="fas fa-cogs mr-2 text-primary-600 dark:text-primary-400"></i>
|
<i class="fas fa-exchange-alt w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||||
Transfer Configurations
|
Transfer Configurations
|
||||||
</h1>
|
</h1>
|
||||||
<a href="/configs/new" class="btn-primary">
|
<a href="/configs/new" class="flex items-center justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
<i class="fas fa-plus mr-2"></i>
|
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||||
New Configuration
|
New Configuration
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
if len(data.Configs) == 0 {
|
if len(data.Configs) == 0 {
|
||||||
<div class="card p-12 flex flex-col items-center justify-center text-center">
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 p-8 flex flex-col items-center justify-center text-center">
|
||||||
<div class="inline-block p-4 rounded-full bg-secondary-100 dark:bg-secondary-700 mb-4">
|
<div class="inline-flex h-16 w-16 flex-shrink-0 items-center justify-center rounded-full bg-gray-100 mb-4 dark:bg-gray-700">
|
||||||
<i class="fas fa-folder-open text-secondary-400 dark:text-secondary-500 text-3xl"></i>
|
<i class="fas fa-exchange-alt text-gray-400 dark:text-gray-500 text-3xl"></i>
|
||||||
</div>
|
|
||||||
<h3 class="mt-2 text-lg font-medium text-secondary-900 dark:text-secondary-100">No configurations</h3>
|
|
||||||
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">Get started by creating a new transfer configuration.</p>
|
|
||||||
<div class="mt-6">
|
|
||||||
<a href="/configs/new" class="btn-primary">
|
|
||||||
<i class="fas fa-plus mr-2"></i>
|
|
||||||
Create First Configuration
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">No configurations</h3>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 mb-4">Get started by creating a new transfer configuration.</p>
|
||||||
|
<a href="/configs/new" class="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||||
|
Create First Configuration
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
<div class="card overflow-hidden">
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 overflow-hidden">
|
||||||
<ul role="list" class="divide-y divide-secondary-200 dark:divide-secondary-700">
|
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
for _, config := range data.Configs {
|
for _, config := range data.Configs {
|
||||||
<li>
|
<li>
|
||||||
<div class="block hover:bg-secondary-50 dark:hover:bg-secondary-750 transition-colors">
|
<div class="block hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||||
<div class="px-4 py-4 sm:px-6">
|
<div class="px-4 py-4 sm:px-6">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<p class="text-sm font-medium text-primary-600 dark:text-primary-400 truncate">
|
<p class="text-sm font-medium text-blue-600 dark:text-blue-400 truncate">
|
||||||
{ config.Name }
|
{ config.Name }
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Google Drive Authentication Badge -->
|
<!-- Google Drive Authentication Badge -->
|
||||||
if (config.DestinationType == "gdrive" || config.SourceType == "gdrive" || config.SourceType == "gphotos" || config.DestinationType == "gphotos") && !config.GetGoogleAuthenticated() {
|
if (config.DestinationType == "gdrive" || config.SourceType == "gdrive" || config.SourceType == "gphotos" || config.DestinationType == "gphotos") && !config.GetGoogleAuthenticated() {
|
||||||
<span class="ml-2 px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100">
|
<span class="ml-2 bg-yellow-100 text-yellow-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded-full dark:bg-yellow-900 dark:text-yellow-300">
|
||||||
<i class="fas fa-exclamation-triangle mr-1 flex items-center"></i>
|
<i class="fas fa-exclamation-triangle w-3 h-3 mr-1 inline"></i>
|
||||||
Authentication Required
|
Authentication Required
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Google Drive Authentication Status Indicator -->
|
<!-- Google Drive Authentication Status Indicator -->
|
||||||
if (config.DestinationType == "gdrive" || config.SourceType == "gdrive" || config.SourceType == "gphotos" || config.DestinationType == "gphotos") && config.GetGoogleAuthenticated() {
|
if (config.DestinationType == "gdrive" || config.SourceType == "gdrive" || config.SourceType == "gphotos" || config.DestinationType == "gphotos") && config.GetGoogleAuthenticated() {
|
||||||
<span class="ml-2 px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100">
|
<span class="ml-2 bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300">
|
||||||
<i class="fas fa-check-circle mr-1 flex items-center"></i>
|
<i class="fas fa-check-circle w-3 h-3 mr-1 inline"></i>
|
||||||
Authenticated
|
Authenticated
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -288,22 +412,31 @@ templ Configs(ctx context.Context, data ConfigsData) {
|
|||||||
<div class="ml-2 flex-shrink-0 flex space-x-2">
|
<div class="ml-2 flex-shrink-0 flex space-x-2">
|
||||||
<!-- Google Drive Authentication Button -->
|
<!-- Google Drive Authentication Button -->
|
||||||
if (config.DestinationType == "gdrive" || config.SourceType == "gdrive" || config.SourceType == "gphotos" || config.DestinationType == "gphotos") && !config.GetGoogleAuthenticated() {
|
if (config.DestinationType == "gdrive" || config.SourceType == "gdrive" || config.SourceType == "gphotos" || config.DestinationType == "gphotos") && !config.GetGoogleAuthenticated() {
|
||||||
<a href={ templ.SafeURL(fmt.Sprintf("/configs/%d/gdrive-auth", config.ID)) } class="btn-warning btn-sm">
|
<a href={ templ.SafeURL(fmt.Sprintf("/configs/%d/gdrive-auth", config.ID)) } class="text-yellow-700 bg-yellow-100 hover:bg-yellow-200 focus:ring-4 focus:outline-none focus:ring-yellow-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-yellow-900 dark:text-yellow-300 dark:hover:bg-yellow-800 dark:focus:ring-yellow-800">
|
||||||
<i class="fab fa-google mr-1"></i>
|
<i class="fas fa-key w-3.5 h-3.5 mr-1.5"></i>
|
||||||
Authenticate
|
Authenticate
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
|
||||||
<a href={ templ.SafeURL(fmt.Sprintf("/configs/%d", config.ID)) } class="btn-secondary btn-sm">
|
<a href={ templ.SafeURL(fmt.Sprintf("/configs/%d", config.ID)) } class="text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 dark:focus:ring-gray-700">
|
||||||
<i class="fas fa-edit mr-1"></i>
|
<i class="fas fa-edit w-3.5 h-3.5 mr-1.5"></i>
|
||||||
Edit
|
Edit
|
||||||
</a>
|
</a>
|
||||||
|
<!-- Duplicate config button -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
hx-post={ fmt.Sprintf("/configs/%d/duplicate", config.ID) }
|
||||||
|
hx-target="body"
|
||||||
|
class="text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:ring-4 focus:outline-none focus:ring-indigo-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-indigo-700 dark:text-indigo-300 dark:hover:bg-indigo-600 dark:focus:ring-indigo-800">
|
||||||
|
<i class="fas fa-clone w-3.5 h-3.5 mr-1.5"></i>
|
||||||
|
Duplicate
|
||||||
|
</button>
|
||||||
<!-- Add delete dialog for each configuration -->
|
<!-- Add delete dialog for each configuration -->
|
||||||
@ConfigDialog(
|
@ConfigDialog(
|
||||||
fmt.Sprintf("delete-config-dialog-%d", config.ID),
|
fmt.Sprintf("delete-config-dialog-%d", config.ID),
|
||||||
"Delete Configuration",
|
"Delete Configuration",
|
||||||
fmt.Sprintf("Are you sure you want to delete the configuration '%s'? This cannot be undone.", config.Name),
|
fmt.Sprintf("Are you sure you want to delete the configuration '%s'? This cannot be undone.", config.Name),
|
||||||
"btn-danger",
|
"text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center",
|
||||||
"Delete",
|
"Delete",
|
||||||
"delete",
|
"delete",
|
||||||
config.ID,
|
config.ID,
|
||||||
@@ -311,26 +444,26 @@ templ Configs(ctx context.Context, data ConfigsData) {
|
|||||||
)
|
)
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={ showConfigDialog(fmt.Sprintf("delete-config-dialog-%d", config.ID)) }
|
onclick={ showModal(fmt.Sprintf("delete-config-dialog-%d", config.ID)) }
|
||||||
class="btn-danger btn-sm">
|
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800">
|
||||||
<i class="fas fa-trash-alt mr-1"></i>
|
<i class="fas fa-trash-alt w-3.5 h-3.5 mr-1.5"></i>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 sm:flex sm:justify-between">
|
<div class="mt-3 sm:flex sm:justify-between">
|
||||||
<div class="sm:flex">
|
<div class="sm:flex flex-col md:flex-row gap-2 md:gap-6">
|
||||||
<p class="flex items-center text-sm text-secondary-500 dark:text-secondary-400">
|
<p class="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
<i class="fas fa-upload flex-shrink-0 mr-1.5 h-5 w-5 text-secondary-400 dark:text-secondary-500"></i>
|
<i class="fas fa-upload w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||||
Source: { config.SourceType }: { config.SourcePath }
|
Source: { config.SourceType }: { config.SourcePath }
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2 flex items-center text-sm text-secondary-500 dark:text-secondary-400 sm:mt-0 sm:ml-6">
|
<p class="mt-2 md:mt-0 flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
<i class="fas fa-download flex-shrink-0 mr-1.5 h-5 w-5 text-secondary-400 dark:text-secondary-500"></i>
|
<i class="fas fa-download w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||||
Destination: { config.DestinationType }: { config.DestinationPath }
|
Destination: { config.DestinationType }: { config.DestinationPath }
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 flex items-center text-sm text-secondary-500 dark:text-secondary-400 sm:mt-0">
|
<div class="mt-2 md:mt-0 flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
<i class="fas fa-calendar-alt flex-shrink-0 mr-1.5 h-5 w-5 text-secondary-400 dark:text-secondary-500"></i>
|
<i class="far fa-clock w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||||
<p>
|
<p>
|
||||||
Updated: { config.UpdatedAt.Format("2006-01-02 15:04:05") }
|
Updated: { config.UpdatedAt.Format("2006-01-02 15:04:05") }
|
||||||
</p>
|
</p>
|
||||||
@@ -345,22 +478,56 @@ templ Configs(ctx context.Context, data ConfigsData) {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Help Notice -->
|
<!-- Help Section -->
|
||||||
<div class="mt-8 text-center">
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg shadow-sm mt-8 p-4 border border-gray-200 dark:border-gray-700">
|
||||||
<p class="text-sm text-secondary-500 dark:text-secondary-400">
|
<div class="flex items-start mb-2">
|
||||||
<i class="fas fa-info-circle mr-1 text-primary-500"></i>
|
<div class="flex items-center h-5">
|
||||||
Configurations define how files are transferred between systems
|
<i class="fas fa-info-circle w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<div class="ml-2 text-sm">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">Configurations define how files are transferred between systems</p>
|
||||||
<!-- Google Drive Auth Help -->
|
</div>
|
||||||
<div class="mt-4 text-center">
|
</div>
|
||||||
<p class="text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fab fa-google mr-1 text-blue-500 inline-flex items-center"></i>
|
<div class="flex items-start mt-4">
|
||||||
Google Drive and Google Photos configurations require authentication. Click the "Authenticate" button to complete setup.
|
<div class="flex items-center h-5">
|
||||||
</p>
|
<i class="fab fa-google-drive w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 text-sm">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">Google Drive and Google Photos configurations require authentication. Click the "Authenticate" button to complete setup.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start mt-4">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<i class="fas fa-clone w-4 h-4 text-indigo-500 dark:text-indigo-400 mr-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 text-sm">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">When duplicating configurations, you may need to re-enter sensitive credentials for security reasons. Make sure to edit duplicated configurations to add any required credentials.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Set dark background color if in dark mode
|
||||||
|
if (document.documentElement.classList.contains('dark')) {
|
||||||
|
document.getElementById('configs-container').style.backgroundColor = '#111827';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener for theme changes
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener('click', function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
document.getElementById('configs-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+184
-92
@@ -4,7 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/starfleetcptn/gomft/internal/db"
|
"github.com/starfleetcptn/gomft/internal/db"
|
||||||
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DashboardData struct {
|
type DashboardData struct {
|
||||||
@@ -13,19 +15,44 @@ type DashboardData struct {
|
|||||||
CompletedToday int
|
CompletedToday int
|
||||||
FailedTransfers int
|
FailedTransfers int
|
||||||
Configs map[uint]db.TransferConfig
|
Configs map[uint]db.TransferConfig
|
||||||
|
RcloneVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRcloneVersion executes the rclone --version command and returns the version string
|
||||||
|
func GetRcloneVersion() string {
|
||||||
|
cmd := exec.Command("rclone", "--version")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the version from output (typically first line contains "rclone v1.XX.X")
|
||||||
|
outputStr := string(output)
|
||||||
|
lines := strings.Split(outputStr, "\n")
|
||||||
|
if len(lines) > 0 {
|
||||||
|
parts := strings.Split(lines[0], " ")
|
||||||
|
for _, part := range parts {
|
||||||
|
if strings.HasPrefix(part, "v") {
|
||||||
|
return part
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
templ Dashboard(ctx context.Context, data DashboardData) {
|
templ Dashboard(ctx context.Context, data DashboardData) {
|
||||||
@LayoutWithContext("Dashboard", ctx) {
|
@LayoutWithContext("Dashboard", ctx) {
|
||||||
<div class="py-6">
|
<div id="dashboard-container" style="min-height: 100vh;" class="bg-gray-50 dark:bg-gray-900">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="pb-8 w-full">
|
||||||
<div class="flex items-center justify-between mb-8">
|
<!-- Page Header -->
|
||||||
<h1 class="text-3xl font-bold text-secondary-900 dark:text-secondary-100">
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
<i class="fas fa-tachometer-alt mr-2 text-primary-600 dark:text-primary-400"></i>
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-chart-pie w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||||
Dashboard
|
Dashboard
|
||||||
</h1>
|
</h1>
|
||||||
<div class="text-sm text-secondary-500 dark:text-secondary-400">
|
<div class="bg-white dark:bg-gray-800 p-2 px-4 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
<span id="current-date" class="font-medium"></span>
|
<span id="current-date" class="font-medium text-gray-700 dark:text-gray-300"></span>
|
||||||
<script>
|
<script>
|
||||||
document.getElementById('current-date').textContent = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
document.getElementById('current-date').textContent = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||||
</script>
|
</script>
|
||||||
@@ -33,154 +60,199 @@ templ Dashboard(ctx context.Context, data DashboardData) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stats Overview Cards -->
|
<!-- Stats Overview Cards -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
||||||
<div class="card overflow-hidden">
|
<!-- Active Transfers -->
|
||||||
<div class="p-6 flex items-center">
|
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 sm:p-6">
|
||||||
<div class="rounded-full bg-blue-100 dark:bg-blue-900 p-3 mr-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<i class="fas fa-exchange-alt text-blue-600 dark:text-blue-300 text-xl"></i>
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Active Transfers</h3>
|
||||||
|
<div class="inline-flex items-center justify-center w-10 h-10 text-blue-600 bg-blue-100 rounded-lg dark:text-blue-300 dark:bg-blue-900">
|
||||||
|
<i class="fas fa-exchange-alt w-6 h-6 flex items-center justify-center"></i>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<p class="text-sm font-medium text-secondary-500 dark:text-secondary-400">Active Transfers</p>
|
<div class="flex items-center">
|
||||||
<p class="text-2xl font-bold text-secondary-900 dark:text-secondary-100">{ strconv.Itoa(data.ActiveTransfers) }</p>
|
<div class="flex-shrink-0">
|
||||||
|
<span class="text-4xl font-bold leading-none text-gray-900 dark:text-white">{ strconv.Itoa(data.ActiveTransfers) }</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 flex items-baseline text-sm font-medium">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Currently in progress</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card overflow-hidden">
|
<!-- Completed Today -->
|
||||||
<div class="p-6 flex items-center">
|
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 sm:p-6">
|
||||||
<div class="rounded-full bg-green-100 dark:bg-green-900 p-3 mr-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<i class="fas fa-check-circle text-green-600 dark:text-green-300 text-xl"></i>
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Completed Today</h3>
|
||||||
|
<div class="inline-flex items-center justify-center w-10 h-10 text-green-600 bg-green-100 rounded-lg dark:text-green-300 dark:bg-green-900">
|
||||||
|
<i class="fas fa-check-circle w-6 h-6 flex items-center justify-center"></i>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<p class="text-sm font-medium text-secondary-500 dark:text-secondary-400">Completed Today</p>
|
<div class="flex items-center">
|
||||||
<p class="text-2xl font-bold text-secondary-900 dark:text-secondary-100">{ strconv.Itoa(data.CompletedToday) }</p>
|
<div class="flex-shrink-0">
|
||||||
|
<span class="text-4xl font-bold leading-none text-gray-900 dark:text-white">{ strconv.Itoa(data.CompletedToday) }</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 flex items-baseline text-sm font-medium">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Successfully completed</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card overflow-hidden">
|
<!-- Failed Transfers -->
|
||||||
<div class="p-6 flex items-center">
|
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 sm:p-6">
|
||||||
<div class="rounded-full bg-red-100 dark:bg-red-900 p-3 mr-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<i class="fas fa-exclamation-circle text-red-600 dark:text-red-300 text-xl"></i>
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Failed Transfers</h3>
|
||||||
|
<div class="inline-flex items-center justify-center w-10 h-10 text-red-600 bg-red-100 rounded-lg dark:text-red-300 dark:bg-red-900">
|
||||||
|
<i class="fas fa-exclamation-circle w-6 h-6 flex items-center justify-center"></i>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<p class="text-sm font-medium text-secondary-500 dark:text-secondary-400">Failed Transfers</p>
|
<div class="flex items-center">
|
||||||
<p class="text-2xl font-bold text-secondary-900 dark:text-secondary-100">{ strconv.Itoa(data.FailedTransfers) }</p>
|
<div class="flex-shrink-0">
|
||||||
|
<span class="text-4xl font-bold leading-none text-gray-900 dark:text-white">{ strconv.Itoa(data.FailedTransfers) }</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 flex items-baseline text-sm font-medium">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Require attention</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
<!-- Main Content Area -->
|
||||||
<div class="card">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
|
||||||
<div class="card-header">
|
<!-- Recent Jobs Card -->
|
||||||
<h3 class="text-lg font-medium text-secondary-900 dark:text-secondary-100">
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||||
<i class="fas fa-history mr-2 text-primary-500"></i>
|
<div class="flex items-center justify-between px-4 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-history w-6 h-6 mr-2 text-blue-500 dark:text-blue-400 flex-shrink-0"></i>
|
||||||
Recent Jobs
|
Recent Jobs
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="p-4">
|
||||||
if len(data.RecentJobs) == 0 {
|
if len(data.RecentJobs) == 0 {
|
||||||
<div class="py-8 text-center">
|
<!-- Empty state with Flowbite styling -->
|
||||||
<div class="inline-block p-4 rounded-full bg-secondary-100 dark:bg-secondary-800 mb-4">
|
<div class="flex flex-col items-center justify-center py-8 px-4 text-center">
|
||||||
<i class="fas fa-inbox text-secondary-400 text-3xl"></i>
|
<div class="inline-flex h-16 w-16 flex-shrink-0 items-center justify-center rounded-full bg-gray-100 mb-4 dark:bg-gray-700">
|
||||||
|
<i class="fas fa-clipboard-list text-gray-500 dark:text-gray-400 text-3xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-secondary-500 dark:text-secondary-400">No recent jobs found</p>
|
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">No recent jobs found</h3>
|
||||||
<a href="/jobs/new" class="mt-4 inline-flex items-center text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300">
|
<p class="text-gray-500 dark:text-gray-400 mb-4">Get started by creating your first job.</p>
|
||||||
<span>Create your first job</span>
|
<a href="/jobs/new" class="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
<i class="fas fa-arrow-right ml-1"></i>
|
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||||
|
Create your first job
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
<div class="flow-root">
|
<div class="flow-root">
|
||||||
<ul role="list" class="-my-5 divide-y divide-secondary-200 dark:divide-secondary-700">
|
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
for _, job := range data.RecentJobs {
|
for _, job := range data.RecentJobs {
|
||||||
<li class="py-4 hover:bg-secondary-50 dark:hover:bg-secondary-800 px-4 rounded-lg transition-colors">
|
<li class="py-3 sm:py-4 hover:bg-gray-50 dark:hover:bg-gray-700 px-3 rounded-lg transition-colors">
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
if job.Status == "completed" {
|
if job.Status == "completed" {
|
||||||
<span class="h-10 w-10 rounded-full bg-green-100 flex items-center justify-center dark:bg-green-900">
|
<span class="inline-flex items-center justify-center h-12 w-12 rounded-full text-green-600 bg-green-100 dark:bg-green-900">
|
||||||
<i class="fas fa-check text-green-600 dark:text-green-300"></i>
|
<i class="fas fa-check-circle w-6 h-6 flex items-center justify-center"></i>
|
||||||
</span>
|
</span>
|
||||||
} else if job.Status == "failed" {
|
} else if job.Status == "failed" {
|
||||||
<span class="h-10 w-10 rounded-full bg-red-100 flex items-center justify-center dark:bg-red-900">
|
<span class="inline-flex items-center justify-center h-12 w-12 rounded-full text-red-600 bg-red-100 dark:bg-red-900">
|
||||||
<i class="fas fa-times text-red-600 dark:text-red-300"></i>
|
<i class="fas fa-exclamation-circle w-6 h-6 flex items-center justify-center"></i>
|
||||||
</span>
|
</span>
|
||||||
} else {
|
} else {
|
||||||
<span class="h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center dark:bg-blue-900">
|
<span class="inline-flex items-center justify-center h-12 w-12 rounded-full text-blue-600 bg-blue-100 dark:bg-blue-900">
|
||||||
<i class="fas fa-sync-alt text-blue-600 dark:text-blue-300"></i>
|
<i class="fas fa-tasks w-6 h-6 flex items-center justify-center"></i>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0 ms-4">
|
||||||
<p class="text-sm font-medium text-secondary-900 truncate dark:text-secondary-100">
|
<p class="text-sm font-medium text-gray-900 truncate dark:text-white">
|
||||||
{ getConfigNameForHistory(job, data.Configs) }
|
{ getConfigNameForHistory(job, data.Configs) }
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center mt-1">
|
<div class="flex items-center mt-1">
|
||||||
<i class="fas fa-clock text-xs text-secondary-500 dark:text-secondary-400 mr-1"></i>
|
<i class="fas fa-clock w-4 h-4 text-gray-500 dark:text-gray-400 mr-1"></i>
|
||||||
<p class="text-sm text-secondary-500 truncate dark:text-secondary-400">
|
<p class="text-sm text-gray-500 truncate dark:text-gray-400">
|
||||||
Started: { job.StartTime.Format("Jan 02, 2006 15:04:05") }
|
Started: { job.StartTime.Format("Jan 02, 2006 15:04:05") }
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<a href={ templ.SafeURL(fmt.Sprintf("/job-runs/%d", job.ID)) }
|
||||||
<a href={ templ.SafeURL(fmt.Sprintf("/job-runs/%d", job.ID)) }
|
class="inline-flex items-center text-sm font-medium text-blue-600 hover:underline dark:text-blue-500">
|
||||||
class="inline-flex items-center px-3 py-1.5 border border-secondary-300 text-sm leading-5 font-medium rounded-full text-secondary-700 bg-white hover:bg-secondary-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 dark:bg-secondary-800 dark:text-secondary-200 dark:border-secondary-600 dark:hover:bg-secondary-700">
|
View
|
||||||
<i class="fas fa-eye mr-1"></i>
|
<i class="fas fa-arrow-right w-3 h-3 ms-2"></i>
|
||||||
View
|
</a>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6">
|
<div class="pt-4 border-t border-gray-200 dark:border-gray-700 mt-4">
|
||||||
<a href="/jobs" class="w-full flex justify-center items-center px-4 py-2 border border-secondary-300 shadow-sm text-sm font-medium rounded-lg text-secondary-700 bg-white hover:bg-secondary-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 dark:bg-secondary-800 dark:text-secondary-200 dark:border-secondary-600 dark:hover:bg-secondary-700">
|
<a href="/jobs" class="inline-flex items-center justify-center w-full px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-list-ul mr-2"></i>
|
<i class="fas fa-list w-4 h-4 mr-2"></i>
|
||||||
View all jobs
|
View all jobs
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<!-- Quick Actions Card -->
|
||||||
<div class="card-header">
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||||
<h3 class="text-lg font-medium text-secondary-900 dark:text-secondary-100">
|
<div class="flex items-center justify-between px-4 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
<i class="fas fa-bolt mr-2 text-primary-500"></i>
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-bolt w-6 h-6 mr-2 text-blue-500 dark:text-blue-400 flex-shrink-0"></i>
|
||||||
Quick Actions
|
Quick Actions
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body space-y-4">
|
<div class="p-4 space-y-4">
|
||||||
<a href="/configs/new" class="btn-primary text-center flex items-center justify-center">
|
<a href="/configs/new" class="inline-flex items-center justify-center w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
<i class="fas fa-plus-circle mr-2"></i>
|
<i class="fas fa-plus w-5 h-5 mr-2"></i>
|
||||||
Create New Config
|
Create New Config
|
||||||
</a>
|
</a>
|
||||||
<a href="/jobs/new" class="btn-primary text-center flex items-center justify-center">
|
<a href="/jobs/new" class="inline-flex items-center justify-center w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
<i class="fas fa-play-circle mr-2"></i>
|
<i class="fas fa-tasks w-5 h-5 mr-2"></i>
|
||||||
Create New Job
|
Create New Job
|
||||||
</a>
|
</a>
|
||||||
<a href="/history" class="btn-secondary text-center flex items-center justify-center">
|
<a href="/history" class="inline-flex items-center justify-center w-full text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg px-5 py-2.5 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700">
|
||||||
<i class="fas fa-history mr-2"></i>
|
<i class="fas fa-history w-5 h-5 mr-2"></i>
|
||||||
View Transfer History
|
View Transfer History
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- System Status Card -->
|
<!-- System Status Card with Flowbite design -->
|
||||||
<div class="mt-6 bg-secondary-50 dark:bg-secondary-800 rounded-lg p-4 border border-secondary-200 dark:border-secondary-700">
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm mt-6">
|
||||||
<h4 class="text-sm font-medium text-secondary-900 dark:text-secondary-100 mb-2">System Status</h4>
|
<div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 rounded-t-lg border-b border-gray-200 dark:border-gray-600">
|
||||||
<div class="space-y-2">
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white">System Status</h4>
|
||||||
<div class="flex items-center justify-between">
|
</div>
|
||||||
<span class="text-xs text-secondary-500 dark:text-secondary-400">Server</span>
|
<div class="px-4 py-3">
|
||||||
<span class="badge badge-success">Online</span>
|
<div class="space-y-3">
|
||||||
</div>
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center justify-between">
|
<span class="text-sm text-gray-700 dark:text-gray-300">Server</span>
|
||||||
<span class="text-xs text-secondary-500 dark:text-secondary-400">Scheduler</span>
|
<span class="bg-green-100 text-green-800 text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300">
|
||||||
<span class="badge badge-success">Running</span>
|
<span class="w-2 h-2 mr-1 bg-green-500 rounded-full"></span>
|
||||||
</div>
|
Online
|
||||||
<div class="flex items-center justify-between">
|
</span>
|
||||||
<span class="text-xs text-secondary-500 dark:text-secondary-400">Database</span>
|
</div>
|
||||||
<span class="badge badge-success">Connected</span>
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-300">Scheduler</span>
|
||||||
|
<span class="bg-green-100 text-green-800 text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300">
|
||||||
|
<span class="w-2 h-2 mr-1 bg-green-500 rounded-full"></span>
|
||||||
|
Running
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-300">Database</span>
|
||||||
|
<span class="bg-green-100 text-green-800 text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300">
|
||||||
|
<span class="w-2 h-2 mr-1 bg-green-500 rounded-full"></span>
|
||||||
|
Connected
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-300">Rclone</span>
|
||||||
|
<span class="bg-blue-100 text-blue-800 text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded-full dark:bg-blue-900 dark:text-blue-300">
|
||||||
|
<i class="fas fa-terminal w-3 h-3 mr-1"></i>
|
||||||
|
if data.RcloneVersion != "" {
|
||||||
|
{ data.RcloneVersion }
|
||||||
|
} else {
|
||||||
|
Unknown
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,6 +260,26 @@ templ Dashboard(ctx context.Context, data DashboardData) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Set dark background color if in dark mode
|
||||||
|
if (document.documentElement.classList.contains('dark')) {
|
||||||
|
document.getElementById('dashboard-container').style.backgroundColor = '#111827';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener for theme changes
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener('click', function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
document.getElementById('dashboard-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/starfleetcptn/gomft/internal/db"
|
||||||
|
"time"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationsData struct {
|
||||||
|
Notifications []db.UserNotification
|
||||||
|
UnreadCount int64
|
||||||
|
// Pagination fields
|
||||||
|
CurrentPage int
|
||||||
|
TotalPages int
|
||||||
|
TotalCount int
|
||||||
|
PerPage int
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatNotificationTime formats a notification time in a user-friendly way
|
||||||
|
func FormatNotificationTime(t time.Time) string {
|
||||||
|
now := time.Now()
|
||||||
|
diff := now.Sub(t)
|
||||||
|
|
||||||
|
if diff < time.Minute {
|
||||||
|
return "just now"
|
||||||
|
} else if diff < time.Hour {
|
||||||
|
minutes := int(diff.Minutes())
|
||||||
|
if minutes == 1 {
|
||||||
|
return "1 minute ago"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d minutes ago", minutes)
|
||||||
|
} else if diff < 24*time.Hour {
|
||||||
|
hours := int(diff.Hours())
|
||||||
|
if hours == 1 {
|
||||||
|
return "1 hour ago"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d hours ago", hours)
|
||||||
|
} else if diff < 48*time.Hour {
|
||||||
|
return "yesterday"
|
||||||
|
} else {
|
||||||
|
days := int(diff.Hours() / 24)
|
||||||
|
if days < 7 {
|
||||||
|
return fmt.Sprintf("%d days ago", days)
|
||||||
|
} else {
|
||||||
|
return t.Format("Jan 2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNotificationIcon returns the appropriate icon class based on notification type
|
||||||
|
func GetNotificationIcon(notificationType db.NotificationType) string {
|
||||||
|
switch notificationType {
|
||||||
|
case db.NotificationJobStart:
|
||||||
|
return "fas fa-play text-blue-600 dark:text-blue-300"
|
||||||
|
case db.NotificationJobComplete:
|
||||||
|
return "fas fa-check-circle text-green-600 dark:text-green-300"
|
||||||
|
case db.NotificationJobFail:
|
||||||
|
return "fas fa-exclamation-circle text-red-600 dark:text-red-300"
|
||||||
|
case db.NotificationConfigUpdate:
|
||||||
|
return "fas fa-cog text-blue-600 dark:text-blue-300"
|
||||||
|
case db.NotificationSystemAlert:
|
||||||
|
return "fas fa-bell text-yellow-600 dark:text-yellow-300"
|
||||||
|
default:
|
||||||
|
return "fas fa-info-circle text-blue-600 dark:text-blue-300"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNotificationBgColor returns the appropriate background color class based on notification type
|
||||||
|
func GetNotificationBgColor(notificationType db.NotificationType) string {
|
||||||
|
switch notificationType {
|
||||||
|
case db.NotificationJobStart:
|
||||||
|
return "bg-blue-100 dark:bg-blue-900"
|
||||||
|
case db.NotificationJobComplete:
|
||||||
|
return "bg-green-100 dark:bg-green-900"
|
||||||
|
case db.NotificationJobFail:
|
||||||
|
return "bg-red-100 dark:bg-red-900"
|
||||||
|
case db.NotificationConfigUpdate:
|
||||||
|
return "bg-blue-100 dark:bg-blue-900"
|
||||||
|
case db.NotificationSystemAlert:
|
||||||
|
return "bg-yellow-100 dark:bg-yellow-900"
|
||||||
|
default:
|
||||||
|
return "bg-blue-100 dark:bg-blue-900"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNotificationBadgeColor returns the appropriate badge color class based on notification type
|
||||||
|
func GetNotificationBadgeColor(notificationType db.NotificationType) string {
|
||||||
|
switch notificationType {
|
||||||
|
case db.NotificationJobStart:
|
||||||
|
return "bg-primary-700"
|
||||||
|
case db.NotificationJobComplete:
|
||||||
|
return "bg-green-600"
|
||||||
|
case db.NotificationJobFail:
|
||||||
|
return "bg-red-600"
|
||||||
|
case db.NotificationConfigUpdate:
|
||||||
|
return "bg-blue-500"
|
||||||
|
case db.NotificationSystemAlert:
|
||||||
|
return "bg-yellow-500"
|
||||||
|
default:
|
||||||
|
return "bg-primary-700"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNotificationBadgeIcon returns the appropriate badge icon class based on notification type
|
||||||
|
func GetNotificationBadgeIcon(notificationType db.NotificationType) string {
|
||||||
|
switch notificationType {
|
||||||
|
case db.NotificationJobStart:
|
||||||
|
return "fas fa-play"
|
||||||
|
case db.NotificationJobComplete:
|
||||||
|
return "fas fa-check"
|
||||||
|
case db.NotificationJobFail:
|
||||||
|
return "fas fa-times"
|
||||||
|
case db.NotificationConfigUpdate:
|
||||||
|
return "fas fa-wrench"
|
||||||
|
case db.NotificationSystemAlert:
|
||||||
|
return "fas fa-exclamation"
|
||||||
|
default:
|
||||||
|
return "fas fa-info"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ NotificationDropdown(data NotificationsData) {
|
||||||
|
<div
|
||||||
|
class="block py-2 px-4 text-base font-medium text-center text-gray-700 bg-gray-50 dark:bg-gray-600 dark:text-gray-300">
|
||||||
|
Notifications
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
if len(data.Notifications) == 0 {
|
||||||
|
<div class="py-4 px-4 text-center text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-bell-slash text-2xl mb-2"></i>
|
||||||
|
<p>No notifications</p>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
for _, notification := range data.Notifications {
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL(notification.Link) }
|
||||||
|
class={ "flex py-3 px-4 border-b hover:bg-gray-100 dark:hover:bg-gray-600 dark:border-gray-600", templ.KV("bg-blue-50 dark:bg-blue-900/20", !notification.IsRead) }>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class={ "w-11 h-11 rounded-full flex items-center justify-center", GetNotificationBgColor(notification.Type) }>
|
||||||
|
<i class={ GetNotificationIcon(notification.Type) }></i>
|
||||||
|
</div>
|
||||||
|
// <div class={ "flex absolute justify-center items-center ml-6 -mt-5 w-5 h-5 rounded-full border border-white dark:border-gray-700", GetNotificationBadgeColor(notification.Type) }>
|
||||||
|
// <i class={ "text-white text-xs", GetNotificationBadgeIcon(notification.Type) }></i>
|
||||||
|
// </div>
|
||||||
|
</div>
|
||||||
|
<div class="pl-3 w-full">
|
||||||
|
<div class="text-gray-500 font-normal text-sm mb-1.5 dark:text-gray-400">
|
||||||
|
<span class="font-semibold text-gray-900 dark:text-white">{ notification.Title }:</span> { notification.Message }
|
||||||
|
</div>
|
||||||
|
<div class="text-xs font-medium text-primary-600 dark:text-primary-500">
|
||||||
|
{ FormatNotificationTime(notification.CreatedAt) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<a href="/notifications" class="block w-1/2 py-2 text-md font-medium text-center text-gray-900 bg-gray-50 hover:bg-gray-100 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-500">
|
||||||
|
<div class="inline-flex items-center">
|
||||||
|
<i class="far fa-eye mr-2"></i>
|
||||||
|
View all
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="/notifications/mark-all-read"
|
||||||
|
hx-post="/notifications/mark-all-read"
|
||||||
|
hx-swap="none"
|
||||||
|
hx-target="#notification-count"
|
||||||
|
class="block w-1/2 py-2 text-md font-medium text-center text-gray-900 bg-gray-50 hover:bg-gray-100 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-500 border-l border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="inline-flex items-center">
|
||||||
|
<i class="fas fa-check-double mr-2"></i>
|
||||||
|
Mark all read
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ NotificationCount(count int64) {
|
||||||
|
if count > 0 {
|
||||||
|
<div class="absolute inline-flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500 rounded-full -top-1 -right-1" id="notification-count">
|
||||||
|
if count > 99 {
|
||||||
|
99+
|
||||||
|
} else {
|
||||||
|
{ fmt.Sprintf("%d", count) }
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Note: To implement pagination, update the NotificationsData struct in notifications.templ
|
||||||
|
// to include these fields:
|
||||||
|
// CurrentPage int
|
||||||
|
// TotalPages int
|
||||||
|
// TotalCount int
|
||||||
|
// PerPage int
|
||||||
|
|
||||||
|
templ NotificationsPage(ctx context.Context, data NotificationsData) {
|
||||||
|
@LayoutWithContext("Notifications", ctx) {
|
||||||
|
<div class="py-6 px-4 mx-auto max-w-7xl lg:px-8">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-bell w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||||
|
Notifications
|
||||||
|
</h1>
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800 rounded-lg px-4 py-2 inline-flex items-center">
|
||||||
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
|
<span class="font-medium">Total: { fmt.Sprint(data.TotalCount) } notifications</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controls and Filters -->
|
||||||
|
<div class="mb-6 p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center gap-4">
|
||||||
|
<div class="ml-auto">
|
||||||
|
<label class="inline-flex items-center text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
Show
|
||||||
|
<select
|
||||||
|
id="perPage"
|
||||||
|
name="perPage"
|
||||||
|
class="mx-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
hx-get={ fmt.Sprintf("/notifications?page=1&perPage=%s", "${this.value}") }
|
||||||
|
hx-target="body"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-trigger="change"
|
||||||
|
>
|
||||||
|
<option value="10" selected?={ data.PerPage == 10 }>10</option>
|
||||||
|
<option value="25" selected?={ data.PerPage == 25 }>25</option>
|
||||||
|
<option value="50" selected?={ data.PerPage == 50 }>50</option>
|
||||||
|
<option value="100" selected?={ data.PerPage == 100 }>100</option>
|
||||||
|
</select>
|
||||||
|
entries
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="/notifications/mark-all-read"
|
||||||
|
hx-post="/notifications/mark-all-read"
|
||||||
|
hx-swap="none"
|
||||||
|
hx-target="#notification-bell"
|
||||||
|
class="inline-flex items-center py-2.5 px-4 text-sm font-medium text-white bg-blue-700 rounded-lg border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-check-double w-4 h-4 mr-2"></i>
|
||||||
|
Mark all as read
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
if len(data.Notifications) == 0 {
|
||||||
|
<div class="p-8 text-center bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="flex justify-center mb-4">
|
||||||
|
<span class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gray-100 dark:bg-gray-700">
|
||||||
|
<i class="fas fa-bell-slash text-gray-400 dark:text-gray-500 text-3xl"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">No notifications</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">You don't have any notifications yet.</p>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 overflow-hidden">
|
||||||
|
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
for _, notification := range data.Notifications {
|
||||||
|
<li id={ "notification-" + fmt.Sprintf("%d", notification.ID) }
|
||||||
|
class={ templ.KV("bg-blue-50 dark:bg-blue-900/20", !notification.IsRead) }>
|
||||||
|
<div class="block transition-colors px-4 py-4 sm:px-6">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex-shrink-0 mr-4">
|
||||||
|
<div class={ "w-12 h-12 rounded-full flex items-center justify-center", GetNotificationBgColor(notification.Type) }>
|
||||||
|
<i class={ GetNotificationIcon(notification.Type) }></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base font-medium text-gray-900 dark:text-white">{ notification.Title }</h3>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{ notification.Message }</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<span>{ FormatNotificationTime(notification.CreatedAt) }</span>
|
||||||
|
if !notification.IsRead {
|
||||||
|
<button
|
||||||
|
hx-post={ "/notifications/" + fmt.Sprintf("%d", notification.ID) + "/read" }
|
||||||
|
hx-target={ "#notification-" + fmt.Sprintf("%d", notification.ID) }
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
class="ml-4 text-blue-700 bg-blue-100 hover:bg-blue-200 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:text-blue-400 dark:bg-blue-900/30 dark:hover:bg-blue-900/40 dark:focus:ring-blue-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-check w-3.5 h-3.5 mr-1.5"></i>
|
||||||
|
Mark as read
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL(notification.Link) }
|
||||||
|
class="text-blue-700 bg-blue-100 hover:bg-blue-200 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:text-blue-400 dark:bg-blue-900/30 dark:hover:bg-blue-900/40 dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-eye w-3.5 h-3.5 mr-1.5"></i>
|
||||||
|
View details
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
if data.TotalPages > 1 {
|
||||||
|
<div class="mt-6 flex flex-col sm:flex-row justify-between items-center py-4 px-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<!-- Mobile pagination -->
|
||||||
|
<div class="flex-1 flex justify-between gap-4 sm:hidden w-full mb-4 sm:mb-0">
|
||||||
|
if data.CurrentPage > 1 {
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL("/notifications?page=" + strconv.Itoa(data.CurrentPage-1) + "&perPage=" + strconv.Itoa(data.PerPage)) }
|
||||||
|
hx-get={ "/notifications?page=" + strconv.Itoa(data.CurrentPage-1) + "&perPage=" + strconv.Itoa(data.PerPage) }
|
||||||
|
hx-target="body"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class="w-full inline-flex justify-center items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
<i class="fas fa-chevron-left mr-1"></i> Previous
|
||||||
|
</a>
|
||||||
|
} else {
|
||||||
|
<span class="w-full inline-flex justify-center items-center px-4 py-2 text-sm font-medium text-gray-400 bg-gray-100 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-600 dark:text-gray-600 cursor-not-allowed">
|
||||||
|
<i class="fas fa-chevron-left mr-1"></i> Previous
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.CurrentPage < data.TotalPages {
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL("/notifications?page=" + strconv.Itoa(data.CurrentPage+1) + "&perPage=" + strconv.Itoa(data.PerPage)) }
|
||||||
|
hx-get={ "/notifications?page=" + strconv.Itoa(data.CurrentPage+1) + "&perPage=" + strconv.Itoa(data.PerPage) }
|
||||||
|
hx-target="body"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class="w-full inline-flex justify-center items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
Next <i class="fas fa-chevron-right ml-1"></i>
|
||||||
|
</a>
|
||||||
|
} else {
|
||||||
|
<span class="w-full inline-flex justify-center items-center px-4 py-2 text-sm font-medium text-gray-400 bg-gray-100 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-600 dark:text-gray-600 cursor-not-allowed">
|
||||||
|
Next <i class="fas fa-chevron-right ml-1"></i>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop pagination -->
|
||||||
|
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
Showing
|
||||||
|
<span class="font-medium">{ fmt.Sprint((data.CurrentPage-1)*data.PerPage + 1) }</span>
|
||||||
|
to
|
||||||
|
<span class="font-medium">{ fmt.Sprint(minInt((data.CurrentPage)*data.PerPage, data.TotalCount)) }</span>
|
||||||
|
of
|
||||||
|
<span class="font-medium">{ fmt.Sprint(data.TotalCount) }</span>
|
||||||
|
notifications
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<nav class="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
||||||
|
<!-- First page -->
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL("/notifications?page=1&perPage=" + strconv.Itoa(data.PerPage)) }
|
||||||
|
hx-get={ "/notifications?page=1&perPage=" + strconv.Itoa(data.PerPage) }
|
||||||
|
hx-target="body"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class={ "relative inline-flex items-center justify-center rounded-l-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700", templ.KV("opacity-50 pointer-events-none", data.CurrentPage == 1) }
|
||||||
|
>
|
||||||
|
<span class="sr-only">First</span>
|
||||||
|
<i class="fas fa-angle-double-left text-xs"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Previous page -->
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL("/notifications?page=" + strconv.Itoa(maxInt(1, data.CurrentPage-1)) + "&perPage=" + strconv.Itoa(data.PerPage)) }
|
||||||
|
hx-get={ "/notifications?page=" + strconv.Itoa(maxInt(1, data.CurrentPage-1)) + "&perPage=" + strconv.Itoa(data.PerPage) }
|
||||||
|
hx-target="body"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class={ "relative inline-flex items-center justify-center border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700", templ.KV("opacity-50 pointer-events-none", data.CurrentPage == 1) }
|
||||||
|
>
|
||||||
|
<span class="sr-only">Previous</span>
|
||||||
|
<i class="fas fa-angle-left text-xs"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Page numbers -->
|
||||||
|
@notificationPageNumbers(data.CurrentPage, data.TotalPages, data.PerPage)
|
||||||
|
|
||||||
|
<!-- Next page -->
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL("/notifications?page=" + strconv.Itoa(minInt(data.TotalPages, data.CurrentPage+1)) + "&perPage=" + strconv.Itoa(data.PerPage)) }
|
||||||
|
hx-get={ "/notifications?page=" + strconv.Itoa(minInt(data.TotalPages, data.CurrentPage+1)) + "&perPage=" + strconv.Itoa(data.PerPage) }
|
||||||
|
hx-target="body"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class={ "relative inline-flex items-center justify-center border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700", templ.KV("opacity-50 pointer-events-none", data.CurrentPage == data.TotalPages) }
|
||||||
|
>
|
||||||
|
<span class="sr-only">Next</span>
|
||||||
|
<i class="fas fa-angle-right text-xs"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Last page -->
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL("/notifications?page=" + strconv.Itoa(data.TotalPages) + "&perPage=" + strconv.Itoa(data.PerPage)) }
|
||||||
|
hx-get={ "/notifications?page=" + strconv.Itoa(data.TotalPages) + "&perPage=" + strconv.Itoa(data.PerPage) }
|
||||||
|
hx-target="body"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class={ "relative inline-flex items-center justify-center rounded-r-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700", templ.KV("opacity-50 pointer-events-none", data.CurrentPage == data.TotalPages) }
|
||||||
|
>
|
||||||
|
<span class="sr-only">Last</span>
|
||||||
|
<i class="fas fa-angle-double-right text-xs"></i>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Help Section -->
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg shadow-sm mt-8 p-4 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="flex items-start mb-2">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<i class="fas fa-info-circle w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 text-sm">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">Notifications keep you updated on important system events and alerts.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start mt-4">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<i class="fas fa-bell w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 text-sm">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">Unread notifications are highlighted in blue. Mark them as read to clear the highlight.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start mt-4">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<i class="fas fa-list-ol w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 text-sm">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">Adjust how many notifications to display per page using the dropdown menu.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for pagination math
|
||||||
|
func minInt(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for pagination math
|
||||||
|
func maxInt(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the pageNumbers template for consistent pagination layout
|
||||||
|
templ notificationPageNumbers(currentPage int, totalPages int, perPage int) {
|
||||||
|
// Show at most 5 page numbers with the current page in the middle when possible
|
||||||
|
for i := 1; i <= totalPages; i++ {
|
||||||
|
if i == currentPage {
|
||||||
|
<span aria-current="page" class="relative z-10 inline-flex items-center bg-blue-600 px-4 py-2 text-sm font-medium text-white focus:z-20 border border-blue-600 dark:bg-blue-600">
|
||||||
|
{ fmt.Sprint(i) }
|
||||||
|
</span>
|
||||||
|
} else if i == 1 || i == totalPages || (i >= currentPage-2 && i <= currentPage+2) {
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL("/notifications?page=" + strconv.Itoa(i) + "&perPage=" + strconv.Itoa(perPage)) }
|
||||||
|
hx-get={ "/notifications?page=" + strconv.Itoa(i) + "&perPage=" + strconv.Itoa(perPage) }
|
||||||
|
hx-target="body"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-indicator={ fmt.Sprintf("#page-indicator-%d", i) }
|
||||||
|
class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20 border border-gray-300 bg-white dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700 relative"
|
||||||
|
>
|
||||||
|
{ fmt.Sprint(i) }
|
||||||
|
<span id={ fmt.Sprintf("page-indicator-%d", i) } class="htmx-indicator absolute top-0 right-0 w-4 h-4 -mt-1 -mr-1">
|
||||||
|
<i class="fas fa-spinner fa-spin text-xs"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
} else if i == currentPage-3 || i == currentPage+3 {
|
||||||
|
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 bg-white dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400">
|
||||||
|
...
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrorPage renders a generic error page with customizable title, message, and details
|
||||||
|
templ ErrorPage(ctx context.Context, code int, title, message, details string) {
|
||||||
|
@LayoutWithContext(fmt.Sprintf("Error %d - %s", code, title), ctx) {
|
||||||
|
<div class="min-h-screen flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="w-full max-w-lg bg-white dark:bg-gray-800 shadow-lg rounded-lg overflow-hidden">
|
||||||
|
<div class="px-6 py-8">
|
||||||
|
<div class="text-center">
|
||||||
|
if code == 404 {
|
||||||
|
<div class="mx-auto flex items-center justify-center h-24 w-24 rounded-full bg-blue-100 dark:bg-blue-900">
|
||||||
|
<svg class="h-14 w-14 text-blue-600 dark:text-blue-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064"></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
} else if code == 401 {
|
||||||
|
<div class="mx-auto flex items-center justify-center h-24 w-24 rounded-full bg-yellow-100 dark:bg-yellow-900">
|
||||||
|
<svg class="h-14 w-14 text-yellow-600 dark:text-yellow-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
} else if code == 500 {
|
||||||
|
<div class="mx-auto flex items-center justify-center h-24 w-24 rounded-full bg-red-100 dark:bg-red-900">
|
||||||
|
<svg class="h-14 w-14 text-red-600 dark:text-red-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="mx-auto flex items-center justify-center h-24 w-24 rounded-full bg-gray-100 dark:bg-gray-700">
|
||||||
|
<svg class="h-14 w-14 text-gray-600 dark:text-gray-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1 class="mt-6 text-3xl font-extrabold text-gray-900 dark:text-white">
|
||||||
|
{ fmt.Sprintf("%d - %s", code, title) }
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p class="mt-3 text-lg text-gray-500 dark:text-gray-400">
|
||||||
|
{ message }
|
||||||
|
</p>
|
||||||
|
|
||||||
|
if details != "" {
|
||||||
|
<div class="mt-4 p-4 rounded-md bg-gray-50 dark:bg-gray-700 overflow-auto">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300 font-mono">
|
||||||
|
{ details }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-6 py-4 bg-gray-50 dark:bg-gray-700 border-t border-gray-200 dark:border-gray-600 flex justify-between">
|
||||||
|
<a href="/dashboard" class="text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300">
|
||||||
|
← Go to Dashboard
|
||||||
|
</a>
|
||||||
|
<a href="/" class="text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300">
|
||||||
|
Go to Home →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotFoundError renders a 404 Not Found error page
|
||||||
|
templ NotFoundError(ctx context.Context) {
|
||||||
|
@ErrorPage(ctx, 404, "Page Not Found", "The page you're looking for doesn't exist or has been moved.", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnauthorizedError renders a 401 Unauthorized error page
|
||||||
|
templ UnauthorizedError(ctx context.Context) {
|
||||||
|
@ErrorPage(ctx, 401, "Unauthorized", "You don't have permission to access this page.", "Please log in or contact your administrator if you believe this is a mistake.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerError renders a 500 Internal Server Error page
|
||||||
|
templ ServerError(ctx context.Context, details string) {
|
||||||
|
@ErrorPage(ctx, 500, "Server Error", "Something went wrong on our end.", details)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenericError renders a generic error page
|
||||||
|
templ GenericError(ctx context.Context, title, message, details string) {
|
||||||
|
@ErrorPage(ctx, 0, title, message, details)
|
||||||
|
}
|
||||||
+937
-720
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,256 @@
|
|||||||
|
package details
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"github.com/starfleetcptn/gomft/components"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata/dialog" // Import dialog package
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata/utils" // Import utils package
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileMetadataDetails renders the details view for a file metadata, matching original structure
|
||||||
|
templ FileMetadataDetails(ctx context.Context, data file_metadata.FileMetadataDetailsData) {
|
||||||
|
@components.LayoutWithContext("File Details", ctx) {
|
||||||
|
<!-- Status and Error Messages -->
|
||||||
|
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||||
|
|
||||||
|
@utils.FileMetadataJS() // Include JS for toasts, etc.
|
||||||
|
|
||||||
|
<div id="file-details-container" style="min-height: 100vh;" class="bg-gray-50 dark:bg-gray-900">
|
||||||
|
<div class="pb-8 w-full">
|
||||||
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-file-alt w-6 h-6 mr-2 text-blue-500"></i>
|
||||||
|
File Details: { data.File.FileName }
|
||||||
|
</h1>
|
||||||
|
<a href="/files" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-arrow-left mr-2"></i> Back to Files
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full">
|
||||||
|
<!-- Card header -->
|
||||||
|
<div class="p-4 md:p-5 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h5 class="text-xl font-bold leading-none text-gray-900 dark:text-white">
|
||||||
|
{ data.File.FileName }
|
||||||
|
</h5>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
File ID: { strconv.FormatUint(uint64(data.File.ID), 10) }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card content -->
|
||||||
|
<div class="p-4 md:p-5">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<!-- File Information -->
|
||||||
|
<div>
|
||||||
|
<h6 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-file-alt mr-2 text-gray-500 dark:text-gray-400"></i> File Information
|
||||||
|
</h6>
|
||||||
|
<div class="overflow-x-auto relative shadow-md sm:rounded-lg">
|
||||||
|
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-b dark:border-gray-700">
|
||||||
|
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||||
|
Filename
|
||||||
|
</th>
|
||||||
|
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||||
|
{ data.File.FileName }
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b dark:border-gray-700">
|
||||||
|
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||||
|
Size
|
||||||
|
</th>
|
||||||
|
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||||
|
{ utils.FormatFileSize(data.File.FileSize) }
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b dark:border-gray-700">
|
||||||
|
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||||
|
Hash
|
||||||
|
</th>
|
||||||
|
<td class="py-3 px-4 break-all bg-white dark:bg-gray-800">
|
||||||
|
if data.File.FileHash != "" {
|
||||||
|
<span class="font-mono">{ data.File.FileHash }</span>
|
||||||
|
} else {
|
||||||
|
<span class="text-gray-400 dark:text-gray-500 italic">Not available</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b dark:border-gray-700">
|
||||||
|
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||||
|
<span class={ "text-xs font-medium px-2.5 py-0.5 rounded", utils.GetStatusBadgeClass(data.File.Status) }>
|
||||||
|
{ data.File.Status }
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b dark:border-gray-700">
|
||||||
|
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||||
|
Original Path
|
||||||
|
</th>
|
||||||
|
<td class="py-3 px-4 break-all bg-white dark:bg-gray-800">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-folder mr-2 text-yellow-500"></i>
|
||||||
|
<span>{ data.File.OriginalPath }</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b dark:border-gray-700">
|
||||||
|
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||||
|
Destination Path
|
||||||
|
</th>
|
||||||
|
<td class="py-3 px-4 break-all bg-white dark:bg-gray-800">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-folder-open mr-2 text-blue-500"></i>
|
||||||
|
<span>{ data.File.DestinationPath }</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Processing Information -->
|
||||||
|
<div>
|
||||||
|
<h6 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-cogs mr-2 text-gray-500 dark:text-gray-400"></i> Processing Information
|
||||||
|
</h6>
|
||||||
|
<div class="overflow-x-auto relative shadow-md sm:rounded-lg">
|
||||||
|
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-b dark:border-gray-700">
|
||||||
|
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||||
|
Job
|
||||||
|
</th>
|
||||||
|
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/files/job/%d", data.File.JobID)) } class="font-medium text-blue-600 dark:text-blue-500 hover:underline">
|
||||||
|
{ data.File.Job.Name }
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b dark:border-gray-700">
|
||||||
|
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||||
|
Processed Time
|
||||||
|
</th>
|
||||||
|
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="far fa-clock mr-2 text-gray-500"></i>
|
||||||
|
<span>{ data.File.ProcessedTime.Format("2006-01-02 15:04:05") }</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b dark:border-gray-700">
|
||||||
|
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||||
|
Creation Time
|
||||||
|
</th>
|
||||||
|
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-calendar-plus mr-2 text-green-500"></i>
|
||||||
|
<span>{ data.File.CreationTime.Format("2006-01-02 15:04:05") }</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b dark:border-gray-700">
|
||||||
|
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||||
|
Modification Time
|
||||||
|
</th>
|
||||||
|
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-calendar-alt mr-2 text-purple-500"></i>
|
||||||
|
<span>{ data.File.ModTime.Format("2006-01-02 15:04:05") }</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
if data.File.Status == "error" && data.File.ErrorMessage != "" {
|
||||||
|
<tr class="border-b dark:border-gray-700">
|
||||||
|
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||||
|
Error
|
||||||
|
</th>
|
||||||
|
<td class="py-3 px-4 break-all bg-white dark:bg-gray-800">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<i class="fas fa-exclamation-triangle mt-1 mr-2 text-red-500"></i>
|
||||||
|
<span class="text-red-600 dark:text-red-400">{ data.File.ErrorMessage }</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
<tr class="border-b dark:border-gray-700">
|
||||||
|
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||||
|
Record Created
|
||||||
|
</th>
|
||||||
|
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||||
|
{ data.File.CreatedAt.Format("2006-01-02 15:04:05") }
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b dark:border-gray-700">
|
||||||
|
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||||
|
Record Updated
|
||||||
|
</th>
|
||||||
|
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||||
|
{ data.File.UpdatedAt.Format("2006-01-02 15:04:05") }
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete dialog component call -->
|
||||||
|
@dialog.FileMetadataDialog(
|
||||||
|
fmt.Sprintf("delete-file-dialog-%d", data.File.ID),
|
||||||
|
"Delete File Metadata",
|
||||||
|
fmt.Sprintf("Are you sure you want to delete the metadata for '%s'? This cannot be undone.", data.File.FileName),
|
||||||
|
"text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800", // Use correct classes
|
||||||
|
"Delete",
|
||||||
|
"delete",
|
||||||
|
data.File.ID,
|
||||||
|
data.File.FileName,
|
||||||
|
"details", // Indicate this is from the details view
|
||||||
|
)
|
||||||
|
|
||||||
|
<!-- Action buttons -->
|
||||||
|
<div class="mt-6 flex flex-wrap justify-end gap-3">
|
||||||
|
<a href="/files" class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
|
||||||
|
<i class="fas fa-list mr-2"></i> Back to Files
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={ templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-file-dialog-%d')", data.File.ID)} }
|
||||||
|
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800">
|
||||||
|
<i class="fas fa-trash mr-2"></i> Delete Record
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Set dark background color if in dark mode
|
||||||
|
if (document.documentElement.classList.contains('dark')) {
|
||||||
|
document.getElementById('file-details-container').style.backgroundColor = '#111827';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener for theme changes
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener('click', function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
document.getElementById('file-details-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.833
|
||||||
|
package details
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/starfleetcptn/gomft/components"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata/dialog" // Import dialog package
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata/utils" // Import utils package
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileMetadataDetails renders the details view for a file metadata, matching original structure
|
||||||
|
func FileMetadataDetails(ctx context.Context, data file_metadata.FileMetadataDetailsData) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Status and Error Messages --> <div id=\"toast-container\" class=\"fixed top-5 right-5 z-50 flex flex-col gap-2\"></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = utils.FileMetadataJS().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " <div id=\"file-details-container\" style=\"min-height: 100vh;\" class=\"bg-gray-50 dark:bg-gray-900\"><div class=\"pb-8 w-full\"><div class=\"mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4\"><h1 class=\"text-2xl font-bold text-gray-900 dark:text-white flex items-center\"><i class=\"fas fa-file-alt w-6 h-6 mr-2 text-blue-500\"></i> File Details: ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.FileName)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 26, Col: 40}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</h1><a href=\"/files\" class=\"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-arrow-left mr-2\"></i> Back to Files</a></div><div class=\"bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full\"><!-- Card header --><div class=\"p-4 md:p-5 border-b border-gray-200 dark:border-gray-700\"><h5 class=\"text-xl font-bold leading-none text-gray-900 dark:text-white\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.FileName)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 37, Col: 27}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</h5><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">File ID: ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatUint(uint64(data.File.ID), 10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 40, Col: 62}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</p></div><!-- Card content --><div class=\"p-4 md:p-5\"><div class=\"grid grid-cols-1 md:grid-cols-2 gap-6\"><!-- File Information --><div><h6 class=\"text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center\"><i class=\"fas fa-file-alt mr-2 text-gray-500 dark:text-gray-400\"></i> File Information</h6><div class=\"overflow-x-auto relative shadow-md sm:rounded-lg\"><table class=\"w-full text-sm text-left text-gray-500 dark:text-gray-400\"><tbody><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Filename</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.FileName)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 60, Col: 33}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Size</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(utils.FormatFileSize(data.File.FileSize))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 68, Col: 55}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Hash</th><td class=\"py-3 px-4 break-all bg-white dark:bg-gray-800\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.File.FileHash != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"font-mono\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.FileHash)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 77, Col: 58}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<span class=\"text-gray-400 dark:text-gray-500 italic\">Not available</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Status</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 = []any{"text-xs font-medium px-2.5 py-0.5 rounded", utils.GetStatusBadgeClass(data.File.Status)}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<span class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 string
|
||||||
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var9).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var11 string
|
||||||
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.Status)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 89, Col: 32}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span></td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Original Path</th><td class=\"py-3 px-4 break-all bg-white dark:bg-gray-800\"><div class=\"flex items-center\"><i class=\"fas fa-folder mr-2 text-yellow-500\"></i> <span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var12 string
|
||||||
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.OriginalPath)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 100, Col: 44}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span></div></td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Destination Path</th><td class=\"py-3 px-4 break-all bg-white dark:bg-gray-800\"><div class=\"flex items-center\"><i class=\"fas fa-folder-open mr-2 text-blue-500\"></i> <span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 string
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.DestinationPath)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 111, Col: 47}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span></div></td></tr></tbody></table></div></div><!-- Processing Information --><div><h6 class=\"text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center\"><i class=\"fas fa-cogs mr-2 text-gray-500 dark:text-gray-400\"></i> Processing Information</h6><div class=\"overflow-x-auto relative shadow-md sm:rounded-lg\"><table class=\"w-full text-sm text-left text-gray-500 dark:text-gray-400\"><tbody><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Job</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\"><a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var14 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files/job/%d", data.File.JobID))
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var14)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" class=\"font-medium text-blue-600 dark:text-blue-500 hover:underline\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var15 string
|
||||||
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.Job.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 134, Col: 34}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</a></td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Processed Time</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\"><div class=\"flex items-center\"><i class=\"far fa-clock mr-2 text-gray-500\"></i> <span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var16 string
|
||||||
|
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.ProcessedTime.Format("2006-01-02 15:04:05"))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 145, Col: 75}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</span></div></td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Creation Time</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\"><div class=\"flex items-center\"><i class=\"fas fa-calendar-plus mr-2 text-green-500\"></i> <span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var17 string
|
||||||
|
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.CreationTime.Format("2006-01-02 15:04:05"))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 156, Col: 74}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span></div></td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Modification Time</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\"><div class=\"flex items-center\"><i class=\"fas fa-calendar-alt mr-2 text-purple-500\"></i> <span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var18 string
|
||||||
|
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.ModTime.Format("2006-01-02 15:04:05"))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 167, Col: 69}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</span></div></td></tr>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.File.Status == "error" && data.File.ErrorMessage != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Error</th><td class=\"py-3 px-4 break-all bg-white dark:bg-gray-800\"><div class=\"flex items-start\"><i class=\"fas fa-exclamation-triangle mt-1 mr-2 text-red-500\"></i> <span class=\"text-red-600 dark:text-red-400\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var19 string
|
||||||
|
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.ErrorMessage)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 179, Col: 84}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</span></div></td></tr>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Record Created</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var20 string
|
||||||
|
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.CreatedAt.Format("2006-01-02 15:04:05"))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 189, Col: 64}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Record Updated</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var21 string
|
||||||
|
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.UpdatedAt.Format("2006-01-02 15:04:05"))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 197, Col: 64}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</td></tr></tbody></table></div></div></div><!-- Delete dialog component call -->")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = dialog.FileMetadataDialog(
|
||||||
|
fmt.Sprintf("delete-file-dialog-%d", data.File.ID),
|
||||||
|
"Delete File Metadata",
|
||||||
|
fmt.Sprintf("Are you sure you want to delete the metadata for '%s'? This cannot be undone.", data.File.FileName),
|
||||||
|
"text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800", // Use correct classes
|
||||||
|
"Delete",
|
||||||
|
"delete",
|
||||||
|
data.File.ID,
|
||||||
|
data.File.FileName,
|
||||||
|
"details", // Indicate this is from the details view
|
||||||
|
).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<!-- Action buttons --><div class=\"mt-6 flex flex-wrap justify-end gap-3\"><a href=\"/files\" class=\"py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700\"><i class=\"fas fa-list mr-2\"></i> Back to Files</a> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-file-dialog-%d')", data.File.ID)})
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<button type=\"button\" onclick=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var22 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-file-dialog-%d')", data.File.ID)}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22.Call)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" class=\"text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800\"><i class=\"fas fa-trash mr-2\"></i> Delete Record</button></div></div></div></div><script>\n\t\t\t\t// Set dark background color if in dark mode\n\t\t\t\tif (document.documentElement.classList.contains('dark')) {\n\t\t\t\t\tdocument.getElementById('file-details-container').style.backgroundColor = '#111827';\n\t\t\t\t}\n\n\t\t\t\t// Add event listener for theme changes\n\t\t\t\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t\t\t\tconst themeToggle = document.getElementById('theme-toggle');\n\t\t\t\t\tif (themeToggle) {\n\t\t\t\t\t\tthemeToggle.addEventListener('click', function() {\n\t\t\t\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t\t\t\tconst isDark = document.documentElement.classList.contains('dark');\n\t\t\t\t\t\t\t\tdocument.getElementById('file-details-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';\n\t\t\t\t\t\t\t}, 50);\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t</script></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = components.LayoutWithContext("File Details", ctx).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package dialog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileMetadataDialog renders a confirmation dialog for file metadata actions
|
||||||
|
templ FileMetadataDialog(id string, title string, message string, confirmClass string, confirmText string, action string, fileID uint, fileName string, section string) {
|
||||||
|
@utils.FileMetadataJS()
|
||||||
|
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
|
||||||
|
<!-- Modal content -->
|
||||||
|
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
|
||||||
|
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||||
|
<div class="p-6 text-center">
|
||||||
|
if action == "delete" {
|
||||||
|
<i class="fas fa-trash-alt text-red-400 text-3xl mb-4"></i>
|
||||||
|
} else {
|
||||||
|
<i class="fas fa-exclamation-triangle text-yellow-400 text-3xl mb-4"></i>
|
||||||
|
}
|
||||||
|
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{ message }</h3>
|
||||||
|
if section == "list" {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-white font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 bg-red-600 hover:bg-red-700 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-500 dark:hover:bg-red-600 dark:focus:ring-red-800"
|
||||||
|
hx-delete={ fmt.Sprintf("/files/%d", fileID) }
|
||||||
|
hx-target={ fmt.Sprintf("#file-row-%d", fileID) }
|
||||||
|
hx-swap="delete"
|
||||||
|
data-file-name={ fileName }
|
||||||
|
data-file-id={ fmt.Sprint(fileID) }
|
||||||
|
id={ fmt.Sprintf("delete-file-btn-%d", fileID) }
|
||||||
|
onclick={ templ.ComponentScript{Call: fmt.Sprintf("triggerFileDelete('%s', %d, '%s')", id, fileID, fileName)} }>
|
||||||
|
{ confirmText }
|
||||||
|
</button>
|
||||||
|
} else {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-white font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 bg-red-600 hover:bg-red-700 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-500 dark:hover:bg-red-600 dark:focus:ring-red-800"
|
||||||
|
hx-delete={ fmt.Sprintf("/files/%d", fileID) }
|
||||||
|
hx-redirect="/files"
|
||||||
|
data-file-name={ fileName }
|
||||||
|
data-file-id={ fmt.Sprint(fileID) }
|
||||||
|
id={ fmt.Sprintf("delete-file-btn-%d", fileID) }
|
||||||
|
onclick={ templ.ComponentScript{Call: fmt.Sprintf("triggerFileDelete('%s', %d, '%s')", id, fileID, fileName)} }>
|
||||||
|
{ confirmText }
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<button type="button" onclick={ templ.ComponentScript{Call: fmt.Sprintf("closeModal('%s')", id)} } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.833
|
||||||
|
package dialog
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileMetadataDialog renders a confirmation dialog for file metadata actions
|
||||||
|
func FileMetadataDialog(id string, title string, message string, confirmClass string, confirmText string, action string, fileID uint, fileName string, section string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = utils.FileMetadataJS().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(id)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 11, Col: 13}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" tabindex=\"-1\" aria-hidden=\"true\" class=\"hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full\"><!-- Backdrop --><div id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s-backdrop", id))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 13, Col: 42}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" class=\"fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm\"></div><!-- Modal content --><div class=\"relative p-4 w-full max-w-md max-h-full mx-auto\"><div class=\"relative bg-white rounded-lg shadow dark:bg-gray-700\"><div class=\"p-6 text-center\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if action == "delete" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<i class=\"fas fa-trash-alt text-red-400 text-3xl mb-4\"></i>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<i class=\"fas fa-exclamation-triangle text-yellow-400 text-3xl mb-4\"></i>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<h3 class=\"mb-5 text-lg font-normal text-gray-500 dark:text-gray-400\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(message)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 23, Col: 84}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</h3>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if section == "list" {
|
||||||
|
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("triggerFileDelete('%s', %d, '%s')", id, fileID, fileName)})
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<button type=\"button\" class=\"text-white font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 bg-red-600 hover:bg-red-700 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-500 dark:hover:bg-red-600 dark:focus:ring-red-800\" hx-delete=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/files/%d", fileID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 28, Col: 51}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" hx-target=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("#file-row-%d", fileID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 29, Col: 54}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" hx-swap=\"delete\" data-file-name=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fileName)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 31, Col: 32}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" data-file-id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(fileID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 32, Col: 40}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 string
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("delete-file-btn-%d", fileID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 33, Col: 53}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" onclick=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("triggerFileDelete('%s', %d, '%s')", id, fileID, fileName)}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10.Call)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var11 string
|
||||||
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(confirmText)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 35, Col: 20}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</button> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("triggerFileDelete('%s', %d, '%s')", id, fileID, fileName)})
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<button type=\"button\" class=\"text-white font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 bg-red-600 hover:bg-red-700 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-500 dark:hover:bg-red-600 dark:focus:ring-red-800\" hx-delete=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var12 string
|
||||||
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/files/%d", fileID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 41, Col: 51}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" hx-redirect=\"/files\" data-file-name=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 string
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fileName)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 43, Col: 32}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" data-file-id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var14 string
|
||||||
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(fileID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 44, Col: 40}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var15 string
|
||||||
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("delete-file-btn-%d", fileID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 45, Col: 53}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" onclick=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var16 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("triggerFileDelete('%s', %d, '%s')", id, fileID, fileName)}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16.Call)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var17 string
|
||||||
|
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(confirmText)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 47, Col: 20}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</button> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("closeModal('%s')", id)})
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<button type=\"button\" onclick=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var18 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("closeModal('%s')", id)}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18.Call)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" class=\"text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600\">Cancel</button></div></div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package list
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||||
|
"github.com/starfleetcptn/gomft/components" // Import the main components package
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileMetadataList renders the list of file metadata
|
||||||
|
templ FileMetadataList(ctx context.Context, data file_metadata.FileMetadataListData) {
|
||||||
|
@components.LayoutWithContext("Files", ctx) { // Call using components package
|
||||||
|
<!-- Status and Error Messages -->
|
||||||
|
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||||
|
|
||||||
|
@utils.FileMetadataJS() // Use capitalized function name
|
||||||
|
|
||||||
|
<div id="list-container" style="min-height: 100vh;" class="bg-gray-50 dark:bg-gray-900">
|
||||||
|
<div class="pb-8 w-full">
|
||||||
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-file-alt w-6 h-6 mr-2 text-blue-500"></i>
|
||||||
|
Files
|
||||||
|
</h1>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<a href="/files/search" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-search mr-2"></i> Advanced Search
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Form -->
|
||||||
|
<div class="p-4 mb-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full">
|
||||||
|
<h5 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Filter Files</h5>
|
||||||
|
<form
|
||||||
|
hx-get="/files/partial"
|
||||||
|
hx-target="#file-list-container"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-indicator="#filter-loading"
|
||||||
|
hx-headers='{"X-HX-Request": "true"}'
|
||||||
|
class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
if data.Job == nil {
|
||||||
|
<div>
|
||||||
|
<label for="job_id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Job</label>
|
||||||
|
<input type="text" id="job_id" name="job_id" value={ data.Filter.JobID } placeholder="Job ID" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div>
|
||||||
|
<label for="status" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Status</label>
|
||||||
|
<select id="status" name="status" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option value="">All Statuses</option>
|
||||||
|
<option value="processed" selected?={ data.Filter.Status == "processed" }>Processed</option>
|
||||||
|
<option value="archived" selected?={ data.Filter.Status == "archived" }>Archived</option>
|
||||||
|
<option value="deleted" selected?={ data.Filter.Status == "deleted" }>Deleted</option>
|
||||||
|
<option value="archived_and_deleted" selected?={ data.Filter.Status == "archived_and_deleted" }>Archived & Deleted</option>
|
||||||
|
<option value="error" selected?={ data.Filter.Status == "error" }>Error</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="filename" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Filename</label>
|
||||||
|
<input type="text" id="filename" name="filename" value={ data.Filter.FileName } placeholder="Filename or partial match"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-3 flex justify-end items-center">
|
||||||
|
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-filter mr-2"></i> Apply Filters
|
||||||
|
</button>
|
||||||
|
<div id="filter-loading" class="htmx-indicator ml-2 flex items-center">
|
||||||
|
<i class="fas fa-circle-notch fa-spin text-blue-600"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full">
|
||||||
|
<!-- Card header -->
|
||||||
|
<div class="p-4 md:p-5 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h5 class="text-xl font-bold leading-none text-gray-900 dark:text-white">
|
||||||
|
File List
|
||||||
|
</h5>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Showing { strconv.FormatInt(int64((data.Page - 1) * data.Limit + 1), 10) } to { strconv.FormatInt(int64(min(data.Page * data.Limit, int(data.TotalCount))), 10) } of { strconv.FormatInt(data.TotalCount, 10) } files
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card content -->
|
||||||
|
<div id="file-list-container" class="p-4 md:p-5">
|
||||||
|
@FileMetadataListPartial(ctx, data, "/files/partial", "#file-list-container")
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Set dark background color if in dark mode
|
||||||
|
if (document.documentElement.classList.contains('dark')) {
|
||||||
|
document.getElementById('list-container').style.backgroundColor = '#111827';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener for theme changes
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener('click', function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
document.getElementById('list-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
package list
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata/dialog"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper function to generate sorting links
|
||||||
|
func sortLink(currentSortBy, currentSortDir, targetSortBy, basePath string, filter file_metadata.FileMetadataFilter, limit int) string {
|
||||||
|
nextSortDir := "asc"
|
||||||
|
if currentSortBy == targetSortBy && currentSortDir == "asc" {
|
||||||
|
nextSortDir = "desc"
|
||||||
|
}
|
||||||
|
|
||||||
|
q := url.Values{}
|
||||||
|
q.Set("page", "1")
|
||||||
|
q.Set("limit", strconv.Itoa(limit))
|
||||||
|
q.Set("sort_by", targetSortBy)
|
||||||
|
q.Set("sort_dir", nextSortDir)
|
||||||
|
if filter.Status != "" {
|
||||||
|
q.Set("status", filter.Status)
|
||||||
|
}
|
||||||
|
if filter.FileName != "" {
|
||||||
|
q.Set("filename", filter.FileName)
|
||||||
|
}
|
||||||
|
if filter.JobID != "" {
|
||||||
|
q.Set("job_id", filter.JobID)
|
||||||
|
}
|
||||||
|
if filter.Hash != "" {
|
||||||
|
q.Set("hash", filter.Hash)
|
||||||
|
}
|
||||||
|
if filter.StartDate != "" {
|
||||||
|
q.Set("start_date", filter.StartDate)
|
||||||
|
}
|
||||||
|
if filter.EndDate != "" {
|
||||||
|
q.Set("end_date", filter.EndDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s?%s", basePath, q.Encode())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get sort icon class
|
||||||
|
func sortIconClass(currentSortBy, currentSortDir, targetSortBy string) string {
|
||||||
|
if currentSortBy == targetSortBy {
|
||||||
|
if currentSortDir == "asc" {
|
||||||
|
return "fas fa-sort-up ml-1"
|
||||||
|
}
|
||||||
|
return "fas fa-sort-down ml-1"
|
||||||
|
}
|
||||||
|
return "fas fa-sort text-gray-400 ml-1"
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileMetadataListPartial renders the list of file metadata in a table format
|
||||||
|
// Added basePath and targetContainerID parameters
|
||||||
|
templ FileMetadataListPartial(ctx context.Context, data file_metadata.FileMetadataListData, basePath string, targetContainerID string) {
|
||||||
|
|
||||||
|
<!-- Container for dynamically generated dialogs -->
|
||||||
|
<div id="dialog-container">
|
||||||
|
for _, file := range data.Files {
|
||||||
|
@dialog.FileMetadataDialog(
|
||||||
|
fmt.Sprintf("delete-file-dialog-%d", file.ID),
|
||||||
|
"Delete File Metadata",
|
||||||
|
fmt.Sprintf("Are you sure you want to delete the metadata for '%s'? This cannot be undone.", file.FileName),
|
||||||
|
"text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800",
|
||||||
|
"Delete",
|
||||||
|
"delete",
|
||||||
|
file.ID,
|
||||||
|
file.FileName,
|
||||||
|
"list",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- File List Table -->
|
||||||
|
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
|
||||||
|
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||||
|
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="px-6 py-3">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
hx-get={ sortLink(data.SortBy, data.SortDir, "id", basePath, data.Filter, data.Limit) }
|
||||||
|
hx-target={ targetContainerID }
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class="flex items-center hover:text-blue-600 dark:hover:text-blue-400"
|
||||||
|
>
|
||||||
|
ID <i class={ sortIconClass(data.SortBy, data.SortDir, "id") }></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
hx-get={ sortLink(data.SortBy, data.SortDir, "filename", basePath, data.Filter, data.Limit) }
|
||||||
|
hx-target={ targetContainerID }
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class="flex items-center hover:text-blue-600 dark:hover:text-blue-400"
|
||||||
|
>
|
||||||
|
Filename <i class={ sortIconClass(data.SortBy, data.SortDir, "filename") }></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
hx-get={ sortLink(data.SortBy, data.SortDir, "size", basePath, data.Filter, data.Limit) }
|
||||||
|
hx-target={ targetContainerID }
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class="flex items-center hover:text-blue-600 dark:hover:text-blue-400"
|
||||||
|
>
|
||||||
|
Size <i class={ sortIconClass(data.SortBy, data.SortDir, "size") }></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
hx-get={ sortLink(data.SortBy, data.SortDir, "processed_time", basePath, data.Filter, data.Limit) }
|
||||||
|
hx-target={ targetContainerID }
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class="flex items-center hover:text-blue-600 dark:hover:text-blue-400"
|
||||||
|
>
|
||||||
|
Processed time <i class={ sortIconClass(data.SortBy, data.SortDir, "processed_time") }></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
hx-get={ sortLink(data.SortBy, data.SortDir, "status", basePath, data.Filter, data.Limit) }
|
||||||
|
hx-target={ targetContainerID }
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class="flex items-center hover:text-blue-600 dark:hover:text-blue-400"
|
||||||
|
>
|
||||||
|
Status <i class={ sortIconClass(data.SortBy, data.SortDir, "status") }></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
if data.Job == nil {
|
||||||
|
<th scope="col" class="px-6 py-3">Job</th>
|
||||||
|
}
|
||||||
|
<th scope="col" class="px-6 py-3">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
for _, file := range data.Files {
|
||||||
|
<tr id={ fmt.Sprintf("file-row-%d", file.ID) } class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600">
|
||||||
|
<td class="px-6 py-4 font-medium text-gray-900 dark:text-white">
|
||||||
|
{ strconv.FormatUint(uint64(file.ID), 10) }
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/files/%d", file.ID)) } class="hover:underline">
|
||||||
|
{ file.FileName }
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
{ utils.FormatFileSize(file.FileSize) }
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
{ file.ProcessedTime.Format("2006-01-02 15:04:05") }
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<span class={ "text-xs font-medium px-2.5 py-0.5 rounded", utils.GetStatusBadgeClass(file.Status) }>
|
||||||
|
{ file.Status }
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
if data.Job == nil {
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
if file.Job.ID > 0 {
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/files/job/%d", file.Job.ID)) } class="font-medium text-blue-600 dark:text-blue-500 hover:underline">
|
||||||
|
{ file.Job.Name }
|
||||||
|
</a>
|
||||||
|
} else {
|
||||||
|
<span class="text-gray-400 dark:text-gray-500 italic">N/A</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/files/%d", file.ID)) } class="font-medium text-blue-600 dark:text-blue-500 hover:underline">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={ templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-file-dialog-%d')", file.ID)} }
|
||||||
|
data-file-id={ strconv.FormatUint(uint64(file.ID), 10) }
|
||||||
|
data-file-name={ file.FileName }
|
||||||
|
class="font-medium text-red-600 dark:text-red-500 hover:underline">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination with HTMX (Update links to include sorting and targetContainerID) -->
|
||||||
|
if data.TotalPages > 1 {
|
||||||
|
<nav class="flex items-center flex-column flex-wrap md:flex-row justify-between p-4" aria-label="Table navigation">
|
||||||
|
<span class="text-sm font-normal text-gray-500 dark:text-gray-400 mb-4 md:mb-0">
|
||||||
|
Showing <span class="font-semibold text-gray-900 dark:text-white">{ strconv.Itoa((data.Page-1)*data.Limit+1) }-{ strconv.Itoa(func() int {
|
||||||
|
end := data.Page*data.Limit
|
||||||
|
if int64(end) > data.TotalCount {
|
||||||
|
return int(data.TotalCount)
|
||||||
|
}
|
||||||
|
return end
|
||||||
|
}()) }</span> of <span class="font-semibold text-gray-900 dark:text-white">{ strconv.FormatInt(data.TotalCount, 10) }</span>
|
||||||
|
</span>
|
||||||
|
<ul class="inline-flex -space-x-px rtl:space-x-reverse text-sm h-8">
|
||||||
|
<li>
|
||||||
|
if data.Page == 1 {
|
||||||
|
<span class="flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-s-lg cursor-not-allowed dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400">
|
||||||
|
Previous
|
||||||
|
</span>
|
||||||
|
} else {
|
||||||
|
<a hx-get={ fmt.Sprintf("%s?page=%d&limit=%d&status=%s&filename=%s&job_id=%s&sort_by=%s&sort_dir=%s", basePath, data.Page - 1, data.Limit, data.Filter.Status, data.Filter.FileName, data.Filter.JobID, data.SortBy, data.SortDir) }
|
||||||
|
hx-target={ targetContainerID }
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class="flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
|
||||||
|
Previous
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
for i := 1; i <= data.TotalPages; i++ {
|
||||||
|
if i == 1 || i == data.TotalPages || (i >= data.Page-2 && i <= data.Page+2) {
|
||||||
|
<li>
|
||||||
|
if i == data.Page {
|
||||||
|
<span aria-current="page" class="flex items-center justify-center px-3 h-8 text-blue-600 border border-gray-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white">
|
||||||
|
{ strconv.Itoa(i) }
|
||||||
|
</span>
|
||||||
|
} else {
|
||||||
|
<a hx-get={ fmt.Sprintf("%s?page=%d&limit=%d&status=%s&filename=%s&job_id=%s&sort_by=%s&sort_dir=%s", basePath, i, data.Limit, data.Filter.Status, data.Filter.FileName, data.Filter.JobID, data.SortBy, data.SortDir) }
|
||||||
|
hx-target={ targetContainerID }
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
|
||||||
|
{ strconv.Itoa(i) }
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
} else if (i == 2 && data.Page > 4) || (i == data.TotalPages-1 && data.Page < data.TotalPages-3) {
|
||||||
|
<li>
|
||||||
|
<span class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400">
|
||||||
|
...
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<li>
|
||||||
|
if data.Page == data.TotalPages {
|
||||||
|
<span class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg cursor-not-allowed dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400">
|
||||||
|
Next
|
||||||
|
</span>
|
||||||
|
} else {
|
||||||
|
<a hx-get={ fmt.Sprintf("%s?page=%d&limit=%d&status=%s&filename=%s&job_id=%s&sort_by=%s&sort_dir=%s", basePath, data.Page + 1, data.Limit, data.Filter.Status, data.Filter.FileName, data.Filter.JobID, data.SortBy, data.SortDir) }
|
||||||
|
hx-target={ targetContainerID }
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
|
||||||
|
Next
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,802 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.833
|
||||||
|
package list
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata/dialog"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper function to generate sorting links
|
||||||
|
func sortLink(currentSortBy, currentSortDir, targetSortBy, basePath string, filter file_metadata.FileMetadataFilter, limit int) string {
|
||||||
|
nextSortDir := "asc"
|
||||||
|
if currentSortBy == targetSortBy && currentSortDir == "asc" {
|
||||||
|
nextSortDir = "desc"
|
||||||
|
}
|
||||||
|
|
||||||
|
q := url.Values{}
|
||||||
|
q.Set("page", "1")
|
||||||
|
q.Set("limit", strconv.Itoa(limit))
|
||||||
|
q.Set("sort_by", targetSortBy)
|
||||||
|
q.Set("sort_dir", nextSortDir)
|
||||||
|
if filter.Status != "" {
|
||||||
|
q.Set("status", filter.Status)
|
||||||
|
}
|
||||||
|
if filter.FileName != "" {
|
||||||
|
q.Set("filename", filter.FileName)
|
||||||
|
}
|
||||||
|
if filter.JobID != "" {
|
||||||
|
q.Set("job_id", filter.JobID)
|
||||||
|
}
|
||||||
|
if filter.Hash != "" {
|
||||||
|
q.Set("hash", filter.Hash)
|
||||||
|
}
|
||||||
|
if filter.StartDate != "" {
|
||||||
|
q.Set("start_date", filter.StartDate)
|
||||||
|
}
|
||||||
|
if filter.EndDate != "" {
|
||||||
|
q.Set("end_date", filter.EndDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s?%s", basePath, q.Encode())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get sort icon class
|
||||||
|
func sortIconClass(currentSortBy, currentSortDir, targetSortBy string) string {
|
||||||
|
if currentSortBy == targetSortBy {
|
||||||
|
if currentSortDir == "asc" {
|
||||||
|
return "fas fa-sort-up ml-1"
|
||||||
|
}
|
||||||
|
return "fas fa-sort-down ml-1"
|
||||||
|
}
|
||||||
|
return "fas fa-sort text-gray-400 ml-1"
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileMetadataListPartial renders the list of file metadata in a table format
|
||||||
|
// Added basePath and targetContainerID parameters
|
||||||
|
func FileMetadataListPartial(ctx context.Context, data file_metadata.FileMetadataListData, basePath string, targetContainerID string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Container for dynamically generated dialogs --><div id=\"dialog-container\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, file := range data.Files {
|
||||||
|
templ_7745c5c3_Err = dialog.FileMetadataDialog(
|
||||||
|
fmt.Sprintf("delete-file-dialog-%d", file.ID),
|
||||||
|
"Delete File Metadata",
|
||||||
|
fmt.Sprintf("Are you sure you want to delete the metadata for '%s'? This cannot be undone.", file.FileName),
|
||||||
|
"text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800",
|
||||||
|
"Delete",
|
||||||
|
"delete",
|
||||||
|
file.ID,
|
||||||
|
file.FileName,
|
||||||
|
"list",
|
||||||
|
).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div><!-- File List Table --><div class=\"relative overflow-x-auto shadow-md sm:rounded-lg\"><table class=\"w-full text-sm text-left text-gray-500 dark:text-gray-400\"><thead class=\"text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400\"><tr><th scope=\"col\" class=\"px-6 py-3\"><a href=\"#\" hx-get=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(sortLink(data.SortBy, data.SortDir, "id", basePath, data.Filter, data.Limit))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 87, Col: 92}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" hx-target=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 88, Col: 36}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" hx-swap=\"innerHTML\" class=\"flex items-center hover:text-blue-600 dark:hover:text-blue-400\">ID ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 = []any{sortIconClass(data.SortBy, data.SortDir, "id")}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<i class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var4).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\"></i></a></th><th scope=\"col\" class=\"px-6 py-3\"><a href=\"#\" hx-get=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(sortLink(data.SortBy, data.SortDir, "filename", basePath, data.Filter, data.Limit))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 98, Col: 98}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" hx-target=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 99, Col: 36}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" hx-swap=\"innerHTML\" class=\"flex items-center hover:text-blue-600 dark:hover:text-blue-400\">Filename ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 = []any{sortIconClass(data.SortBy, data.SortDir, "filename")}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<i class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 string
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"></i></a></th><th scope=\"col\" class=\"px-6 py-3\"><a href=\"#\" hx-get=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 string
|
||||||
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(sortLink(data.SortBy, data.SortDir, "size", basePath, data.Filter, data.Limit))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 109, Col: 94}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" hx-target=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var11 string
|
||||||
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 110, Col: 36}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" hx-swap=\"innerHTML\" class=\"flex items-center hover:text-blue-600 dark:hover:text-blue-400\">Size ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var12 = []any{sortIconClass(data.SortBy, data.SortDir, "size")}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<i class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 string
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var12).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"></i></a></th><th scope=\"col\" class=\"px-6 py-3\"><a href=\"#\" hx-get=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var14 string
|
||||||
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(sortLink(data.SortBy, data.SortDir, "processed_time", basePath, data.Filter, data.Limit))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 120, Col: 104}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" hx-target=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var15 string
|
||||||
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 121, Col: 36}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" hx-swap=\"innerHTML\" class=\"flex items-center hover:text-blue-600 dark:hover:text-blue-400\">Processed time ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var16 = []any{sortIconClass(data.SortBy, data.SortDir, "processed_time")}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var16...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<i class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var17 string
|
||||||
|
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var16).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\"></i></a></th><th scope=\"col\" class=\"px-6 py-3\"><a href=\"#\" hx-get=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var18 string
|
||||||
|
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(sortLink(data.SortBy, data.SortDir, "status", basePath, data.Filter, data.Limit))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 131, Col: 96}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" hx-target=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var19 string
|
||||||
|
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 132, Col: 36}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" hx-swap=\"innerHTML\" class=\"flex items-center hover:text-blue-600 dark:hover:text-blue-400\">Status ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var20 = []any{sortIconClass(data.SortBy, data.SortDir, "status")}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<i class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var21 string
|
||||||
|
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var20).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\"></i></a></th>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.Job == nil {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<th scope=\"col\" class=\"px-6 py-3\">Job</th>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<th scope=\"col\" class=\"px-6 py-3\">Actions</th></tr></thead> <tbody>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, file := range data.Files {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<tr id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var22 string
|
||||||
|
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("file-row-%d", file.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 147, Col: 49}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" class=\"bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600\"><td class=\"px-6 py-4 font-medium text-gray-900 dark:text-white\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var23 string
|
||||||
|
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatUint(uint64(file.ID), 10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 149, Col: 48}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</td><td class=\"px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white\"><a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var24 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files/%d", file.ID))
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var24)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" class=\"hover:underline\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var25 string
|
||||||
|
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(file.FileName)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 153, Col: 23}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</a></td><td class=\"px-6 py-4\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var26 string
|
||||||
|
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(utils.FormatFileSize(file.FileSize))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 157, Col: 44}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</td><td class=\"px-6 py-4\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var27 string
|
||||||
|
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(file.ProcessedTime.Format("2006-01-02 15:04:05"))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 160, Col: 57}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</td><td class=\"px-6 py-4\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var28 = []any{"text-xs font-medium px-2.5 py-0.5 rounded", utils.GetStatusBadgeClass(file.Status)}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var28...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<span class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var29 string
|
||||||
|
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var28).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var30 string
|
||||||
|
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(file.Status)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 164, Col: 21}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</span></td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.Job == nil {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<td class=\"px-6 py-4\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if file.Job.ID > 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var31 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files/job/%d", file.Job.ID))
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var31)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\" class=\"font-medium text-blue-600 dark:text-blue-500 hover:underline\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var32 string
|
||||||
|
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(file.Job.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 171, Col: 25}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</a>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<span class=\"text-gray-400 dark:text-gray-500 italic\">N/A</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<td class=\"px-6 py-4\"><div class=\"flex space-x-3\"><a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var33 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files/%d", file.ID))
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var33)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\" class=\"font-medium text-blue-600 dark:text-blue-500 hover:underline\"><i class=\"fas fa-eye\"></i></a> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-file-dialog-%d')", file.ID)})
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<button type=\"button\" onclick=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var34 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-file-dialog-%d')", file.ID)}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34.Call)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\" data-file-id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var35 string
|
||||||
|
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatUint(uint64(file.ID), 10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 186, Col: 63}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\" data-file-name=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var36 string
|
||||||
|
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(file.FileName)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 187, Col: 39}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" class=\"font-medium text-red-600 dark:text-red-500 hover:underline\"><i class=\"fas fa-trash\"></i></button></div></td></tr>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</tbody></table></div><!-- Pagination with HTMX (Update links to include sorting and targetContainerID) -->")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.TotalPages > 1 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<nav class=\"flex items-center flex-column flex-wrap md:flex-row justify-between p-4\" aria-label=\"Table navigation\"><span class=\"text-sm font-normal text-gray-500 dark:text-gray-400 mb-4 md:mb-0\">Showing <span class=\"font-semibold text-gray-900 dark:text-white\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var37 string
|
||||||
|
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa((data.Page-1)*data.Limit + 1))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 203, Col: 112}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "-")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var38 string
|
||||||
|
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(func() int {
|
||||||
|
end := data.Page * data.Limit
|
||||||
|
if int64(end) > data.TotalCount {
|
||||||
|
return int(data.TotalCount)
|
||||||
|
}
|
||||||
|
return end
|
||||||
|
}()))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 209, Col: 8}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</span> of <span class=\"font-semibold text-gray-900 dark:text-white\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var39 string
|
||||||
|
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatInt(data.TotalCount, 10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 209, Col: 119}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</span></span><ul class=\"inline-flex -space-x-px rtl:space-x-reverse text-sm h-8\"><li>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.Page == 1 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<span class=\"flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-s-lg cursor-not-allowed dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400\">Previous</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<a hx-get=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var40 string
|
||||||
|
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s?page=%d&limit=%d&status=%s&filename=%s&job_id=%s&sort_by=%s&sort_dir=%s", basePath, data.Page-1, data.Limit, data.Filter.Status, data.Filter.FileName, data.Filter.JobID, data.SortBy, data.SortDir))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 218, Col: 232}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\" hx-target=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var41 string
|
||||||
|
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 219, Col: 38}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\" hx-swap=\"innerHTML\" class=\"flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white\">Previous</a>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</li>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for i := 1; i <= data.TotalPages; i++ {
|
||||||
|
if i == 1 || i == data.TotalPages || (i >= data.Page-2 && i <= data.Page+2) {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<li>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if i == data.Page {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<span aria-current=\"page\" class=\"flex items-center justify-center px-3 h-8 text-blue-600 border border-gray-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var42 string
|
||||||
|
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(i))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 232, Col: 26}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<a hx-get=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var43 string
|
||||||
|
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s?page=%d&limit=%d&status=%s&filename=%s&job_id=%s&sort_by=%s&sort_dir=%s", basePath, i, data.Limit, data.Filter.Status, data.Filter.FileName, data.Filter.JobID, data.SortBy, data.SortDir))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 235, Col: 222}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\" hx-target=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var44 string
|
||||||
|
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 236, Col: 40}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" hx-swap=\"innerHTML\" class=\"flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var45 string
|
||||||
|
templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(i))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 239, Col: 26}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</a>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</li>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else if (i == 2 && data.Page > 4) || (i == data.TotalPages-1 && data.Page < data.TotalPages-3) {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<li><span class=\"flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400\">...</span></li>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<li>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.Page == data.TotalPages {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "<span class=\"flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg cursor-not-allowed dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400\">Next</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<a hx-get=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var46 string
|
||||||
|
templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s?page=%d&limit=%d&status=%s&filename=%s&job_id=%s&sort_by=%s&sort_dir=%s", basePath, data.Page+1, data.Limit, data.Filter.Status, data.Filter.FileName, data.Filter.JobID, data.SortBy, data.SortDir))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 258, Col: 232}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\" hx-target=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var47 string
|
||||||
|
templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 259, Col: 38}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" hx-swap=\"innerHTML\" class=\"flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white\">Next</a>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</li></ul></nav>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.833
|
||||||
|
package list
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/starfleetcptn/gomft/components" // Import the main components package
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileMetadataList renders the list of file metadata
|
||||||
|
func FileMetadataList(ctx context.Context, data file_metadata.FileMetadataListData) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, " <!-- Status and Error Messages --> <div id=\"toast-container\" class=\"fixed top-5 right-5 z-50 flex flex-col gap-2\"></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = utils.FileMetadataJS().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " <div id=\"list-container\" style=\"min-height: 100vh;\" class=\"bg-gray-50 dark:bg-gray-900\"><div class=\"pb-8 w-full\"><div class=\"mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4\"><h1 class=\"text-2xl font-bold text-gray-900 dark:text-white flex items-center\"><i class=\"fas fa-file-alt w-6 h-6 mr-2 text-blue-500\"></i> Files</h1><div class=\"flex gap-3\"><a href=\"/files/search\" class=\"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-search mr-2\"></i> Advanced Search</a></div></div><!-- Filter Form --><div class=\"p-4 mb-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full\"><h5 class=\"mb-4 text-lg font-semibold text-gray-900 dark:text-white\">Filter Files</h5><form hx-get=\"/files/partial\" hx-target=\"#file-list-container\" hx-swap=\"innerHTML\" hx-indicator=\"#filter-loading\" hx-headers=\"{"X-HX-Request": "true"}\" class=\"grid grid-cols-1 md:grid-cols-3 gap-4\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.Job == nil {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div><label for=\"job_id\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Job</label> <input type=\"text\" id=\"job_id\" name=\"job_id\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.JobID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list.templ`, Line: 46, Col: 78}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" placeholder=\"Job ID\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div><label for=\"status\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Status</label> <select id=\"status\" name=\"status\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"><option value=\"\">All Statuses</option> <option value=\"processed\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.Filter.Status == "processed" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " selected")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, ">Processed</option> <option value=\"archived\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.Filter.Status == "archived" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " selected")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, ">Archived</option> <option value=\"deleted\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.Filter.Status == "deleted" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " selected")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, ">Deleted</option> <option value=\"archived_and_deleted\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.Filter.Status == "archived_and_deleted" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " selected")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ">Archived & Deleted</option> <option value=\"error\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.Filter.Status == "error" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " selected")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, ">Error</option></select></div><div><label for=\"filename\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Filename</label> <input type=\"text\" id=\"filename\" name=\"filename\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.FileName)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list.templ`, Line: 62, Col: 84}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" placeholder=\"Filename or partial match\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"></div><div class=\"md:col-span-3 flex justify-end items-center\"><button type=\"submit\" class=\"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-filter mr-2\"></i> Apply Filters</button><div id=\"filter-loading\" class=\"htmx-indicator ml-2 flex items-center\"><i class=\"fas fa-circle-notch fa-spin text-blue-600\"></i></div></div></form></div><div class=\"bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full\"><!-- Card header --><div class=\"p-4 md:p-5 border-b border-gray-200 dark:border-gray-700\"><h5 class=\"text-xl font-bold leading-none text-gray-900 dark:text-white\">File List</h5><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Showing ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatInt(int64((data.Page-1)*data.Limit+1), 10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list.templ`, Line: 84, Col: 79}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " to ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatInt(int64(min(data.Page*data.Limit, int(data.TotalCount))), 10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list.templ`, Line: 84, Col: 166}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " of ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatInt(data.TotalCount, 10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list.templ`, Line: 84, Col: 212}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " files</p></div><!-- Card content --><div id=\"file-list-container\" class=\"p-4 md:p-5\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = FileMetadataListPartial(ctx, data, "/files/partial", "#file-list-container").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div></div></div><script>\n\t\t\t\t// Set dark background color if in dark mode\n\t\t\t\tif (document.documentElement.classList.contains('dark')) {\n\t\t\t\t\tdocument.getElementById('list-container').style.backgroundColor = '#111827';\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Add event listener for theme changes\n\t\t\t\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t\t\t\tconst themeToggle = document.getElementById('theme-toggle');\n\t\t\t\t\tif (themeToggle) {\n\t\t\t\t\t\tthemeToggle.addEventListener('click', function() {\n\t\t\t\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t\t\t\tconst isDark = document.documentElement.classList.contains('dark');\n\t\t\t\t\t\t\t\tdocument.getElementById('list-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';\n\t\t\t\t\t\t\t}, 50);\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t</script></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = components.LayoutWithContext("Files", ctx).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||||
|
"github.com/starfleetcptn/gomft/components" // Import the main components package
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileMetadataSearch renders the search interface for file metadata
|
||||||
|
templ FileMetadataSearch(ctx context.Context, data file_metadata.FileMetadataSearchData) {
|
||||||
|
@components.LayoutWithContext("Search Files", ctx) {
|
||||||
|
<!-- Status and Error Messages -->
|
||||||
|
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||||
|
|
||||||
|
@utils.FileMetadataJS() // Include JS for toasts, etc.
|
||||||
|
|
||||||
|
<div id="search-container" style="min-height: 100vh;" class="bg-gray-50 dark:bg-gray-900">
|
||||||
|
<div class="pb-8 w-full">
|
||||||
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-search w-6 h-6 mr-2 text-blue-500"></i>
|
||||||
|
Search Files
|
||||||
|
</h1>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<a href="/files" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-arrow-left mr-2"></i> Back to Files
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Advanced Search Form -->
|
||||||
|
<div class="p-4 mb-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full">
|
||||||
|
<h5 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Advanced File Search</h5>
|
||||||
|
<form
|
||||||
|
hx-get="/files/search/partial"
|
||||||
|
hx-target="#search-results-container"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-indicator="#search-form-loading"
|
||||||
|
hx-headers='{"X-HX-Request": "true"}'
|
||||||
|
hx-boost="false"
|
||||||
|
class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="job_id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Job ID</label>
|
||||||
|
<input type="text" id="job_id" name="job_id" value={ data.Filter.JobID } placeholder="Job ID"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="status" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Status</label>
|
||||||
|
<select id="status" name="status" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option value="">All Statuses</option>
|
||||||
|
<option value="processed" selected?={ data.Filter.Status == "processed" }>Processed</option>
|
||||||
|
<option value="archived" selected?={ data.Filter.Status == "archived" }>Archived</option>
|
||||||
|
<option value="deleted" selected?={ data.Filter.Status == "deleted" }>Deleted</option>
|
||||||
|
<option value="archived_and_deleted" selected?={ data.Filter.Status == "archived_and_deleted" }>Archived & Deleted</option>
|
||||||
|
<option value="error" selected?={ data.Filter.Status == "error" }>Error</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="filename" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Filename</label>
|
||||||
|
<input type="text" id="filename" name="filename" value={ data.Filter.FileName } placeholder="Filename or partial match"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="hash" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">File Hash</label>
|
||||||
|
<input type="text" id="hash" name="hash" value={ data.Filter.Hash } placeholder="MD5 hash"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="start_date" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Processed After</label>
|
||||||
|
<input type="date" id="start_date" name="start_date" value={ data.Filter.StartDate }
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="end_date" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Processed Before</label>
|
||||||
|
<input type="date" id="end_date" name="end_date" value={ data.Filter.EndDate }
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-2 flex justify-end">
|
||||||
|
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-search mr-2"></i> Search Files
|
||||||
|
</button>
|
||||||
|
<div id="search-form-loading" class="htmx-indicator ml-2 flex items-center">
|
||||||
|
<i class="fas fa-circle-notch fa-spin text-blue-600"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Results Container (Initially empty, populated by HTMX) -->
|
||||||
|
<div id="search-results-container" class="bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full mt-6">
|
||||||
|
<div class="p-4 md:p-5 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h5 class="text-xl font-bold leading-none text-gray-900 dark:text-white">
|
||||||
|
Search Results
|
||||||
|
</h5>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Enter search criteria above and click "Search Files".
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 md:p-5">
|
||||||
|
<!-- Content will be loaded here by HTMX -->
|
||||||
|
<div class="text-center text-gray-500 dark:text-gray-400 py-8">
|
||||||
|
No results yet.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Set dark background color if in dark mode
|
||||||
|
if (document.documentElement.classList.contains('dark')) {
|
||||||
|
document.getElementById('search-container').style.backgroundColor = '#111827';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener for theme changes
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener('click', function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
document.getElementById('search-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata/list"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileMetadataSearchContent renders only the search results table and pagination
|
||||||
|
templ FileMetadataSearchContent(ctx context.Context, data file_metadata.FileMetadataSearchData) {
|
||||||
|
<!-- Search Results -->
|
||||||
|
<div id="search-results">
|
||||||
|
if len(data.Files) > 0 {
|
||||||
|
@list.FileMetadataListPartial(ctx, file_metadata.FileMetadataListData{
|
||||||
|
Files: data.Files,
|
||||||
|
Page: data.Page,
|
||||||
|
Limit: data.Limit,
|
||||||
|
TotalCount: data.TotalCount,
|
||||||
|
TotalPages: data.TotalPages,
|
||||||
|
Filter: data.Filter, // Pass filter data for pagination links
|
||||||
|
SortBy: data.SortBy,
|
||||||
|
SortDir: data.SortDir,
|
||||||
|
}, "/files/search/partial", "#search-results-container") // Pass correct base path and target ID
|
||||||
|
} else {
|
||||||
|
<div class="p-6 text-center text-gray-500 dark:text-gray-400">
|
||||||
|
<svg class="mx-auto mb-4 w-12 h-12 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||||
|
</svg>
|
||||||
|
<p>No files found matching your search criteria.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.833
|
||||||
|
package search
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata/list"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileMetadataSearchContent renders only the search results table and pagination
|
||||||
|
func FileMetadataSearchContent(ctx context.Context, data file_metadata.FileMetadataSearchData) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Search Results --><div id=\"search-results\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if len(data.Files) > 0 {
|
||||||
|
templ_7745c5c3_Err = list.FileMetadataListPartial(ctx, file_metadata.FileMetadataListData{
|
||||||
|
Files: data.Files,
|
||||||
|
Page: data.Page,
|
||||||
|
Limit: data.Limit,
|
||||||
|
TotalCount: data.TotalCount,
|
||||||
|
TotalPages: data.TotalPages,
|
||||||
|
Filter: data.Filter, // Pass filter data for pagination links
|
||||||
|
SortBy: data.SortBy,
|
||||||
|
SortDir: data.SortDir,
|
||||||
|
}, "/files/search/partial", "#search-results-container").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"p-6 text-center text-gray-500 dark:text-gray-400\"><svg class=\"mx-auto mb-4 w-12 h-12 text-gray-400\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z\"></path></svg><p>No files found matching your search criteria.</p></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.833
|
||||||
|
package search
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/starfleetcptn/gomft/components" // Import the main components package
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||||
|
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileMetadataSearch renders the search interface for file metadata
|
||||||
|
func FileMetadataSearch(ctx context.Context, data file_metadata.FileMetadataSearchData) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Status and Error Messages --> <div id=\"toast-container\" class=\"fixed top-5 right-5 z-50 flex flex-col gap-2\"></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = utils.FileMetadataJS().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " <div id=\"search-container\" style=\"min-height: 100vh;\" class=\"bg-gray-50 dark:bg-gray-900\"><div class=\"pb-8 w-full\"><div class=\"mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4\"><h1 class=\"text-2xl font-bold text-gray-900 dark:text-white flex items-center\"><i class=\"fas fa-search w-6 h-6 mr-2 text-blue-500\"></i> Search Files</h1><div class=\"flex gap-3\"><a href=\"/files\" class=\"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-arrow-left mr-2\"></i> Back to Files</a></div></div><!-- Advanced Search Form --><div class=\"p-4 mb-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full\"><h5 class=\"mb-4 text-lg font-semibold text-gray-900 dark:text-white\">Advanced File Search</h5><form hx-get=\"/files/search/partial\" hx-target=\"#search-results-container\" hx-swap=\"innerHTML\" hx-indicator=\"#search-form-loading\" hx-headers=\"{"X-HX-Request": "true"}\" hx-boost=\"false\" class=\"grid grid-cols-1 md:grid-cols-2 gap-4\"><div><label for=\"job_id\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Job ID</label> <input type=\"text\" id=\"job_id\" name=\"job_id\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.JobID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/search/file_metadata_search.templ`, Line: 45, Col: 77}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" placeholder=\"Job ID\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"></div><div><label for=\"status\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Status</label> <select id=\"status\" name=\"status\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"><option value=\"\">All Statuses</option> <option value=\"processed\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.Filter.Status == "processed" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " selected")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, ">Processed</option> <option value=\"archived\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.Filter.Status == "archived" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " selected")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, ">Archived</option> <option value=\"deleted\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.Filter.Status == "deleted" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " selected")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, ">Deleted</option> <option value=\"archived_and_deleted\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.Filter.Status == "archived_and_deleted" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " selected")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, ">Archived & Deleted</option> <option value=\"error\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.Filter.Status == "error" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " selected")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ">Error</option></select></div><div><label for=\"filename\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Filename</label> <input type=\"text\" id=\"filename\" name=\"filename\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.FileName)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/search/file_metadata_search.templ`, Line: 61, Col: 84}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" placeholder=\"Filename or partial match\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"></div><div><label for=\"hash\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">File Hash</label> <input type=\"text\" id=\"hash\" name=\"hash\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.Hash)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/search/file_metadata_search.templ`, Line: 66, Col: 72}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" placeholder=\"MD5 hash\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"></div><div><label for=\"start_date\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Processed After</label> <input type=\"date\" id=\"start_date\" name=\"start_date\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.StartDate)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/search/file_metadata_search.templ`, Line: 71, Col: 89}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"></div><div><label for=\"end_date\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Processed Before</label> <input type=\"date\" id=\"end_date\" name=\"end_date\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.EndDate)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/search/file_metadata_search.templ`, Line: 76, Col: 83}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"></div><div class=\"md:col-span-2 flex justify-end\"><button type=\"submit\" class=\"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-search mr-2\"></i> Search Files</button><div id=\"search-form-loading\" class=\"htmx-indicator ml-2 flex items-center\"><i class=\"fas fa-circle-notch fa-spin text-blue-600\"></i></div></div></form></div><!-- Results Container (Initially empty, populated by HTMX) --><div id=\"search-results-container\" class=\"bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full mt-6\"><div class=\"p-4 md:p-5 border-b border-gray-200 dark:border-gray-700\"><h5 class=\"text-xl font-bold leading-none text-gray-900 dark:text-white\">Search Results</h5><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Enter search criteria above and click \"Search Files\".</p></div><div class=\"p-4 md:p-5\"><!-- Content will be loaded here by HTMX --><div class=\"text-center text-gray-500 dark:text-gray-400 py-8\">No results yet.</div></div></div></div><script>\n\t\t\t\t// Set dark background color if in dark mode\n\t\t\t\tif (document.documentElement.classList.contains('dark')) {\n\t\t\t\t\tdocument.getElementById('search-container').style.backgroundColor = '#111827';\n\t\t\t\t}\n\n\t\t\t\t// Add event listener for theme changes\n\t\t\t\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t\t\t\tconst themeToggle = document.getElementById('theme-toggle');\n\t\t\t\t\tif (themeToggle) {\n\t\t\t\t\t\tthemeToggle.addEventListener('click', function() {\n\t\t\t\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t\t\t\tconst isDark = document.documentElement.classList.contains('dark');\n\t\t\t\t\t\t\t\tdocument.getElementById('search-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';\n\t\t\t\t\t\t\t}, 50);\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t</script></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = components.LayoutWithContext("Search Files", ctx).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package file_metadata
|
||||||
|
|
||||||
|
import "github.com/starfleetcptn/gomft/internal/db"
|
||||||
|
|
||||||
|
// FileMetadataFilter represents filter parameters for file metadata queries
|
||||||
|
type FileMetadataFilter struct {
|
||||||
|
Status string
|
||||||
|
JobID string
|
||||||
|
FileName string
|
||||||
|
Hash string
|
||||||
|
StartDate string
|
||||||
|
EndDate string
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileMetadataListData contains data for the file metadata list template
|
||||||
|
type FileMetadataListData struct {
|
||||||
|
Files []db.FileMetadata
|
||||||
|
TotalCount int64
|
||||||
|
Page int
|
||||||
|
Limit int
|
||||||
|
TotalPages int
|
||||||
|
Job *db.Job // Optional: if viewing files for a specific job
|
||||||
|
Filter FileMetadataFilter
|
||||||
|
SortBy string // Added for sorting
|
||||||
|
SortDir string // Added for sorting ("asc" or "desc")
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileMetadataDetailsData contains data for the file metadata details template
|
||||||
|
type FileMetadataDetailsData struct {
|
||||||
|
File db.FileMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileMetadataSearchData contains data for the file metadata search template
|
||||||
|
type FileMetadataSearchData struct {
|
||||||
|
Files []db.FileMetadata
|
||||||
|
TotalCount int64
|
||||||
|
Page int
|
||||||
|
Limit int
|
||||||
|
TotalPages int
|
||||||
|
Filter FileMetadataFilter
|
||||||
|
SortBy string // Added for sorting
|
||||||
|
SortDir string // Added for sorting ("asc" or "desc")
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
// FileMetadataJS provides common JavaScript functions for file metadata components,
|
||||||
|
// including HTMX event listeners for delete toasts.
|
||||||
|
templ FileMetadataJS() {
|
||||||
|
<script>
|
||||||
|
// Function to create and show a toast (Defined locally for guaranteed availability)
|
||||||
|
function showToast(message, type = 'info') {
|
||||||
|
const toastContainer = document.getElementById('toast-container');
|
||||||
|
if (!toastContainer) {
|
||||||
|
console.error("Toast container not found!"); // Keep this error log
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create toast element
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.id = 'toast-' + type + '-' + Date.now();
|
||||||
|
// Use classes similar to the original file_metadata.templ for consistency
|
||||||
|
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
||||||
|
toast.role = 'alert';
|
||||||
|
|
||||||
|
// Set toast content based on type
|
||||||
|
let iconClass;
|
||||||
|
if (type === 'success') {
|
||||||
|
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
||||||
|
} else if (type === 'error') {
|
||||||
|
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
||||||
|
} else { // Default to info
|
||||||
|
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set inner HTML with appropriate icon and message
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
||||||
|
${type === 'success'
|
||||||
|
? '<i class="fas fa-check"></i>'
|
||||||
|
: type === 'error'
|
||||||
|
? '<i class="fas fa-exclamation-circle"></i>'
|
||||||
|
: '<i class="fas fa-info-circle"></i>'}
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 text-sm font-normal">${message}</div>
|
||||||
|
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add toast to container
|
||||||
|
toastContainer.appendChild(toast);
|
||||||
|
|
||||||
|
// Trigger animation after a small delay
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('translate-y-16', 'opacity-0');
|
||||||
|
toast.classList.add('translate-y-0', 'opacity-100');
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// Add event listener to close button
|
||||||
|
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
||||||
|
closeButton.addEventListener('click', function() {
|
||||||
|
toast.classList.add('opacity-0', 'translate-y-4');
|
||||||
|
setTimeout(() => { toast.remove(); }, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-remove toast after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.add('opacity-0', 'translate-y-4');
|
||||||
|
setTimeout(() => { toast.remove(); }, 300);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- HTMX Event Listener for Delete Toasts ---
|
||||||
|
|
||||||
|
// Ensure listener is attached only once using a flag
|
||||||
|
if (!window._gomft_fileMetadataListenerAttached) {
|
||||||
|
document.body.addEventListener('htmx:afterRequest', function(event) {
|
||||||
|
const triggerElement = event.detail.elt;
|
||||||
|
|
||||||
|
// Check if the element that triggered this request was the file delete button from the dialog
|
||||||
|
if (triggerElement && triggerElement.id && triggerElement.id.startsWith('delete-file-btn-')) {
|
||||||
|
// Get the path directly from the element's hx-delete attribute
|
||||||
|
const path = triggerElement.getAttribute('hx-delete');
|
||||||
|
// Ensure requestConfig exists before accessing verb (robustness)
|
||||||
|
const method = event.detail.requestConfig ? event.detail.requestConfig.verb : null;
|
||||||
|
|
||||||
|
// Check if the method was delete and the path from the attribute matches the expected pattern
|
||||||
|
if (method === 'delete' && path && path.match(/^\/files\/\d+$/)) {
|
||||||
|
const fileName = triggerElement.getAttribute('data-file-name') || "Unknown"; // Get filename from the button
|
||||||
|
|
||||||
|
// Call the locally defined showToast function
|
||||||
|
if (event.detail.successful) {
|
||||||
|
showToast(`File "${fileName}" metadata deleted successfully`, 'success');
|
||||||
|
} else {
|
||||||
|
let errorMsg = `Failed to delete file "${fileName}" metadata`;
|
||||||
|
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||||
|
try {
|
||||||
|
const responseJson = JSON.parse(event.detail.xhr.responseText);
|
||||||
|
errorMsg = responseJson.error ? `Error: ${responseJson.error}` : `Error: ${event.detail.xhr.responseText}`;
|
||||||
|
} catch (e) {
|
||||||
|
errorMsg = `Error: ${event.detail.xhr.responseText}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showToast(errorMsg, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the flag to true after attaching the listener
|
||||||
|
window._gomft_fileMetadataListenerAttached = true;
|
||||||
|
console.log("[FileMetadataJS] HTMX afterRequest listener attached."); // Log attachment once
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,34 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// GetStatusBadgeClass returns the appropriate CSS class for a file status badge
|
||||||
|
func GetStatusBadgeClass(status string) string {
|
||||||
|
switch status {
|
||||||
|
case "processed":
|
||||||
|
return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300"
|
||||||
|
case "archived":
|
||||||
|
return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300"
|
||||||
|
case "deleted":
|
||||||
|
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300"
|
||||||
|
case "archived_and_deleted":
|
||||||
|
return "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300"
|
||||||
|
case "error":
|
||||||
|
return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300"
|
||||||
|
default:
|
||||||
|
return "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatFileSize formats a file size in bytes to a human-readable string
|
||||||
|
func FormatFileSize(size int64) string {
|
||||||
|
if size < 1024 {
|
||||||
|
return fmt.Sprintf("%d B", size)
|
||||||
|
} else if size < 1024*1024 {
|
||||||
|
return fmt.Sprintf("%.2f KB", float64(size)/1024)
|
||||||
|
} else if size < 1024*1024*1024 {
|
||||||
|
return fmt.Sprintf("%.2f MB", float64(size)/(1024*1024))
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("%.2f GB", float64(size)/(1024*1024*1024))
|
||||||
|
}
|
||||||
|
}
|
||||||
+183
-203
@@ -6,102 +6,91 @@ import (
|
|||||||
|
|
||||||
templ ForgotPassword(ctx context.Context, errorMessage string, successMessage string) {
|
templ ForgotPassword(ctx context.Context, errorMessage string, successMessage string) {
|
||||||
@LayoutWithContext("Forgot Password", ctx) {
|
@LayoutWithContext("Forgot Password", ctx) {
|
||||||
<div class="min-h-[calc(100vh-4rem)] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 bg-secondary-50 dark:bg-secondary-900">
|
<div class="min-h-[calc(100vh-4rem)] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 bg-gray-50 dark:bg-gray-900">
|
||||||
<div class="max-w-md w-full">
|
<div class="max-w-md w-full space-y-8">
|
||||||
<div class="card overflow-hidden shadow-lg">
|
<div class="bg-white dark:bg-gray-800 shadow-lg rounded-lg p-8">
|
||||||
<div class="p-8">
|
<div class="text-center mb-8">
|
||||||
<div class="text-center mb-8">
|
<div class="mx-auto mb-4 flex h-20 w-20 items-center justify-center rounded-full bg-primary-100 dark:bg-primary-900">
|
||||||
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-primary-100 dark:bg-primary-900 mb-4">
|
<i class="fas fa-key text-primary-600 dark:text-primary-400 text-3xl"></i>
|
||||||
<i class="fas fa-key text-primary-600 dark:text-primary-400 text-3xl"></i>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-3xl font-bold text-secondary-900 dark:text-secondary-100">Password Reset</h2>
|
|
||||||
<p class="mt-2 text-secondary-600 dark:text-secondary-400">Enter your email to receive a reset link</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h2 class="text-3xl font-bold text-gray-900 dark:text-white">Password Reset</h2>
|
||||||
if errorMessage != "" {
|
<p class="mt-2 text-gray-600 dark:text-gray-300">Enter your email to receive a reset link</p>
|
||||||
<div class="bg-red-100 dark:bg-red-900 border border-red-400 dark:border-red-700 text-red-700 dark:text-red-300 px-4 py-3 rounded-lg mb-6" role="alert">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<i class="fas fa-exclamation-circle mr-2"></i>
|
|
||||||
<span class="block sm:inline">{ errorMessage }</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
if successMessage != "" {
|
|
||||||
<div class="bg-green-100 dark:bg-green-900 border border-green-400 dark:border-green-700 text-green-700 dark:text-green-300 px-4 py-3 rounded-lg mb-6" role="alert">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<i class="fas fa-check-circle mr-2"></i>
|
|
||||||
<span class="block sm:inline">{ successMessage }</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<form
|
|
||||||
class="space-y-6"
|
|
||||||
method="POST"
|
|
||||||
action="/forgot-password"
|
|
||||||
x-data="{
|
|
||||||
email: '',
|
|
||||||
loading: false,
|
|
||||||
validate() {
|
|
||||||
return this.email && this.email.includes('@');
|
|
||||||
}
|
|
||||||
}">
|
|
||||||
<div>
|
|
||||||
<label for="email" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Email address</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-envelope text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
x-model="email"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="you@example.com"/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
We'll send a password reset link to this email
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn-primary w-full flex justify-center py-3"
|
|
||||||
x-bind:disabled="!validate()"
|
|
||||||
@click="loading = true">
|
|
||||||
<span x-show="!loading" class="flex items-center">
|
|
||||||
<i class="fas fa-paper-plane mr-2"></i>
|
|
||||||
Send Reset Link
|
|
||||||
</span>
|
|
||||||
<span x-show="loading" class="flex items-center">
|
|
||||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
Processing...
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-8 py-4 bg-secondary-50 dark:bg-secondary-800 border-t border-secondary-200 dark:border-secondary-700 text-center">
|
if errorMessage != "" {
|
||||||
<p class="text-sm text-secondary-600 dark:text-secondary-400">
|
<div class="mb-6 flex items-center rounded-lg bg-red-50 p-4 text-red-800 dark:bg-gray-800 dark:text-red-400" role="alert">
|
||||||
<a href="/login" class="text-primary-600 dark:text-primary-400 hover:text-primary-500 dark:hover:text-primary-300 flex items-center justify-center">
|
<i class="fas fa-exclamation-circle mr-2"></i>
|
||||||
<i class="fas fa-arrow-left mr-2"></i>
|
<span class="[&:not(:first-child)]:ms-2">{ errorMessage }</span>
|
||||||
Back to login
|
</div>
|
||||||
</a>
|
}
|
||||||
</p>
|
|
||||||
|
if successMessage != "" {
|
||||||
|
<div class="mb-6 flex items-center rounded-lg bg-green-50 p-4 text-green-800 dark:bg-gray-800 dark:text-green-400" role="alert">
|
||||||
|
<i class="fas fa-check-circle mr-2"></i>
|
||||||
|
<span class="[&:not(:first-child)]:ms-2">{ successMessage }</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="space-y-6"
|
||||||
|
method="POST"
|
||||||
|
action="/forgot-password"
|
||||||
|
x-data="{
|
||||||
|
email: '',
|
||||||
|
loading: false,
|
||||||
|
validate() {
|
||||||
|
return this.email && this.email.includes('@');
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
<div>
|
||||||
|
<label for="email" class="mb-2 block text-sm font-medium text-gray-900 dark:text-white">Email address</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
|
<i class="fas fa-envelope text-gray-500 dark:text-gray-400"></i>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
x-model="email"
|
||||||
|
class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 pl-10 text-gray-900 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500"
|
||||||
|
placeholder="you@example.com"/>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
We'll send a password reset link to this email
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full rounded-lg bg-primary-600 px-5 py-3 text-center text-sm font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
|
||||||
|
x-bind:disabled="!validate()"
|
||||||
|
@click="loading = true">
|
||||||
|
<span x-show="!loading" class="flex items-center justify-center">
|
||||||
|
<i class="fas fa-paper-plane mr-2"></i>
|
||||||
|
Send Reset Link
|
||||||
|
</span>
|
||||||
|
<span x-show="loading" class="flex items-center justify-center">
|
||||||
|
<svg class="me-3 h-5 w-5 animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
Processing...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-6 border-t border-gray-200 px-8 py-4 text-center dark:border-gray-700">
|
||||||
|
<a href="/login" class="inline-flex items-center text-sm font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300">
|
||||||
|
<i class="fas fa-arrow-left mr-2"></i>
|
||||||
|
Back to login
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Security Notice -->
|
<div class="text-center">
|
||||||
<div class="mt-8 text-center">
|
<div class="inline-flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||||
<div class="inline-flex items-center text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-shield-alt mr-2 text-primary-500"></i>
|
<i class="fas fa-shield-alt mr-2 text-primary-500"></i>
|
||||||
<span>Secure, encrypted connection</span>
|
<span>Secure, encrypted connection</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,125 +103,116 @@ templ ForgotPassword(ctx context.Context, errorMessage string, successMessage st
|
|||||||
// Reset password page for when users click the link from their email
|
// Reset password page for when users click the link from their email
|
||||||
templ ResetPassword(ctx context.Context, token string, errorMessage string) {
|
templ ResetPassword(ctx context.Context, token string, errorMessage string) {
|
||||||
@LayoutWithContext("Reset Password", ctx) {
|
@LayoutWithContext("Reset Password", ctx) {
|
||||||
<div class="min-h-[calc(100vh-4rem)] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 bg-secondary-50 dark:bg-secondary-900">
|
<div class="min-h-[calc(100vh-4rem)] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 bg-gray-50 dark:bg-gray-900">
|
||||||
<div class="max-w-md w-full">
|
<div class="max-w-md w-full space-y-8">
|
||||||
<div class="card overflow-hidden shadow-lg">
|
<div class="bg-white dark:bg-gray-800 shadow-lg rounded-lg p-8">
|
||||||
<div class="p-8">
|
<div class="text-center mb-8">
|
||||||
<div class="text-center mb-8">
|
<div class="mx-auto mb-4 flex h-20 w-20 items-center justify-center rounded-full bg-primary-100 dark:bg-primary-900">
|
||||||
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-primary-100 dark:bg-primary-900 mb-4">
|
<i class="fas fa-lock-open text-primary-600 dark:text-primary-400 text-3xl"></i>
|
||||||
<i class="fas fa-lock-open text-primary-600 dark:text-primary-400 text-3xl"></i>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-3xl font-bold text-secondary-900 dark:text-secondary-100">Reset Password</h2>
|
|
||||||
<p class="mt-2 text-secondary-600 dark:text-secondary-400">Create a new password for your account</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h2 class="text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h2>
|
||||||
if errorMessage != "" {
|
<p class="mt-2 text-gray-600 dark:text-gray-300">Create a new password for your account</p>
|
||||||
<div class="bg-red-100 dark:bg-red-900 border border-red-400 dark:border-red-700 text-red-700 dark:text-red-300 px-4 py-3 rounded-lg mb-6" role="alert">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<i class="fas fa-exclamation-circle mr-2"></i>
|
|
||||||
<span class="block sm:inline">{ errorMessage }</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<form
|
|
||||||
class="space-y-6"
|
|
||||||
method="POST"
|
|
||||||
action="/reset-password"
|
|
||||||
x-data="{
|
|
||||||
password: '',
|
|
||||||
confirmPassword: '',
|
|
||||||
loading: false,
|
|
||||||
validate() {
|
|
||||||
return this.password &&
|
|
||||||
this.password.length >= 8 &&
|
|
||||||
this.password === this.confirmPassword;
|
|
||||||
}
|
|
||||||
}">
|
|
||||||
<input type="hidden" name="token" value={token}/>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">New Password</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
x-model="password"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="••••••••"/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
Minimum 8 characters
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="confirm-password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Confirm New Password</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-lock text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id="confirm-password"
|
|
||||||
name="confirm-password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
x-model="confirmPassword"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="••••••••"/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-sm"
|
|
||||||
x-bind:class="{'text-red-500': confirmPassword && password !== confirmPassword, 'text-secondary-500 dark:text-secondary-400': !confirmPassword || password === confirmPassword}">
|
|
||||||
<span x-show="!confirmPassword || password === confirmPassword">
|
|
||||||
Passwords must match
|
|
||||||
</span>
|
|
||||||
<span x-show="confirmPassword && password !== confirmPassword">
|
|
||||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
|
||||||
Passwords do not match
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn-primary w-full flex justify-center py-3"
|
|
||||||
x-bind:disabled="!validate()"
|
|
||||||
@click="loading = true">
|
|
||||||
<span x-show="!loading" class="flex items-center">
|
|
||||||
<i class="fas fa-check-circle mr-2"></i>
|
|
||||||
Reset Password
|
|
||||||
</span>
|
|
||||||
<span x-show="loading" class="flex items-center">
|
|
||||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
Processing...
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-8 py-4 bg-secondary-50 dark:bg-secondary-800 border-t border-secondary-200 dark:border-secondary-700 text-center">
|
if errorMessage != "" {
|
||||||
<p class="text-sm text-secondary-600 dark:text-secondary-400">
|
<div class="mb-6 flex items-center rounded-lg bg-red-50 p-4 text-red-800 dark:bg-gray-800 dark:text-red-400" role="alert">
|
||||||
<a href="/login" class="text-primary-600 dark:text-primary-400 hover:text-primary-500 dark:hover:text-primary-300 flex items-center justify-center">
|
<i class="fas fa-exclamation-circle mr-2"></i>
|
||||||
<i class="fas fa-arrow-left mr-2"></i>
|
<span class="[&:not(:first-child)]:ms-2">{ errorMessage }</span>
|
||||||
Back to login
|
</div>
|
||||||
</a>
|
}
|
||||||
</p>
|
|
||||||
|
<form
|
||||||
|
class="space-y-6"
|
||||||
|
method="POST"
|
||||||
|
action="/reset-password"
|
||||||
|
x-data="{
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
loading: false,
|
||||||
|
validate() {
|
||||||
|
return this.password &&
|
||||||
|
this.password.length >= 8 &&
|
||||||
|
this.password === this.confirmPassword;
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
<input type="hidden" name="token" value={token}/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="password" class="mb-2 block text-sm font-medium text-gray-900 dark:text-white">New Password</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
|
<i class="fas fa-key text-gray-500 dark:text-gray-400"></i>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
x-model="password"
|
||||||
|
class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 pl-10 text-gray-900 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500"
|
||||||
|
placeholder="••••••••"/>
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Minimum 8 characters
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="confirm-password" class="mb-2 block text-sm font-medium text-gray-900 dark:text-white">Confirm New Password</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
|
<i class="fas fa-lock text-gray-500 dark:text-gray-400"></i>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="confirm-password"
|
||||||
|
name="confirm-password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
x-model="confirmPassword"
|
||||||
|
class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 pl-10 text-gray-900 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500"
|
||||||
|
placeholder="••••••••"/>
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 text-sm"
|
||||||
|
x-bind:class="{'text-red-500': confirmPassword && password !== confirmPassword, 'text-gray-500 dark:text-gray-400': !confirmPassword || password === confirmPassword}">
|
||||||
|
<span x-show="!confirmPassword || password === confirmPassword">
|
||||||
|
Passwords must match
|
||||||
|
</span>
|
||||||
|
<span x-show="confirmPassword && password !== confirmPassword">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-1"></i>
|
||||||
|
Passwords do not match
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full rounded-lg bg-primary-600 px-5 py-3 text-center text-sm font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
|
||||||
|
x-bind:disabled="!validate()"
|
||||||
|
@click="loading = true">
|
||||||
|
<span x-show="!loading" class="flex items-center justify-center">
|
||||||
|
<i class="fas fa-check-circle mr-2"></i>
|
||||||
|
Reset Password
|
||||||
|
</span>
|
||||||
|
<span x-show="loading" class="flex items-center justify-center">
|
||||||
|
<svg class="me-3 h-5 w-5 animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
Processing...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-6 border-t border-gray-200 px-8 py-4 text-center dark:border-gray-700">
|
||||||
|
<a href="/login" class="inline-flex items-center text-sm font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300">
|
||||||
|
<i class="fas fa-arrow-left mr-2"></i>
|
||||||
|
Back to login
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Security Notice -->
|
<div class="text-center">
|
||||||
<div class="mt-8 text-center">
|
<div class="inline-flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||||
<div class="inline-flex items-center text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-shield-alt mr-2 text-primary-500"></i>
|
<i class="fas fa-shield-alt mr-2 text-primary-500"></i>
|
||||||
<span>Secure, encrypted connection</span>
|
<span>Secure, encrypted connection</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+150
-96
@@ -49,66 +49,88 @@ func getConfigNameForHistory(history db.JobHistory, configs map[uint]db.Transfer
|
|||||||
// HistoryContent renders only the content part of the history page for HTMX requests
|
// HistoryContent renders only the content part of the history page for HTMX requests
|
||||||
templ HistoryContent(ctx context.Context, data HistoryData) {
|
templ HistoryContent(ctx context.Context, data HistoryData) {
|
||||||
if len(data.History) == 0 {
|
if len(data.History) == 0 {
|
||||||
<div class="text-center py-12 bg-white dark:bg-secondary-800 rounded-lg shadow">
|
<div class="p-8 text-center bg-white dark:bg-gray-800 rounded-lg shadow">
|
||||||
<div class="inline-block p-4 rounded-full bg-secondary-100 dark:bg-secondary-700 mb-4">
|
<div class="flex justify-center mb-4">
|
||||||
<i class="fas fa-inbox text-secondary-400 dark:text-secondary-500 text-3xl"></i>
|
<span class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gray-100 dark:bg-gray-700">
|
||||||
|
<i class="fas fa-inbox text-gray-400 dark:text-gray-500 text-3xl"></i>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="mt-2 text-lg font-medium text-secondary-900 dark:text-secondary-100">No transfer history</h3>
|
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">No transfer history</h3>
|
||||||
if data.SearchTerm != "" {
|
if data.SearchTerm != "" {
|
||||||
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">No results found for "{ data.SearchTerm }". Try a different search term or <button hx-get="/history" hx-target="#history-content" hx-vals={ fmt.Sprintf(`{"pageSize": %d}`, data.PageSize) } class="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300">clear search</button>.</p>
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
No results found for "{ data.SearchTerm }". Try a different search term or
|
||||||
|
<button
|
||||||
|
hx-get="/history"
|
||||||
|
hx-target="#history-content"
|
||||||
|
hx-vals={ fmt.Sprintf(`{"pageSize": %d}`, data.PageSize) }
|
||||||
|
class="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
|
>
|
||||||
|
clear search
|
||||||
|
</button>.
|
||||||
|
</p>
|
||||||
} else if data.CurrentPage > 1 {
|
} else if data.CurrentPage > 1 {
|
||||||
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">No more results on this page. <button hx-get="/history" hx-target="#history-content" hx-vals={ fmt.Sprintf(`{"page": 1, "pageSize": %d}`, data.PageSize) } class="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300">Return to first page</button>.</p>
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
No more results on this page.
|
||||||
|
<button
|
||||||
|
hx-get="/history"
|
||||||
|
hx-target="#history-content"
|
||||||
|
hx-vals={ fmt.Sprintf(`{"page": 1, "pageSize": %d}`, data.PageSize) }
|
||||||
|
class="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
|
>
|
||||||
|
Return to first page
|
||||||
|
</button>.
|
||||||
|
</p>
|
||||||
} else {
|
} else {
|
||||||
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">Transfer history will appear here once jobs have run.</p>
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Transfer history will appear here once jobs have run.</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
<div class="bg-white dark:bg-secondary-800 shadow overflow-hidden rounded-lg">
|
<div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
|
||||||
<ul role="list" class="divide-y divide-secondary-200 dark:divide-secondary-700">
|
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
for _, history := range data.History {
|
for _, history := range data.History {
|
||||||
<li class="hover:bg-secondary-50 dark:hover:bg-secondary-750 transition-colors">
|
<li class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||||
<div class="px-4 py-4 sm:px-6">
|
<div class="px-4 py-4 sm:px-6">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<p class="text-sm font-medium text-primary-600 dark:text-primary-400 truncate">{ getConfigNameForHistory(history, data.Configs) }</p>
|
<p class="text-sm font-medium text-blue-600 dark:text-blue-400 truncate">{ getConfigNameForHistory(history, data.Configs) }</p>
|
||||||
if history.Status == "completed" {
|
if history.Status == "completed" {
|
||||||
<span class="ml-2 px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-300">
|
<span class="ml-2 px-2.5 py-0.5 inline-flex text-xs font-medium rounded-full bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-300">
|
||||||
Completed
|
Completed
|
||||||
</span>
|
</span>
|
||||||
} else if history.Status == "failed" {
|
} else if history.Status == "failed" {
|
||||||
<span class="ml-2 px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-300">
|
<span class="ml-2 px-2.5 py-0.5 inline-flex text-xs font-medium rounded-full bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-300">
|
||||||
Failed
|
Failed
|
||||||
</span>
|
</span>
|
||||||
} else {
|
} else {
|
||||||
<span class="ml-2 px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-300">
|
<span class="ml-2 px-2.5 py-0.5 inline-flex text-xs font-medium rounded-full bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-300">
|
||||||
{ history.Status }
|
{ history.Status }
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a href={ templ.SafeURL(fmt.Sprintf("/job-runs/%d", history.ID)) }
|
<a href={ templ.SafeURL(fmt.Sprintf("/job-runs/%d", history.ID)) }
|
||||||
class="inline-flex items-center px-3 py-1.5 border border-secondary-300 dark:border-secondary-600 text-sm leading-5 font-medium rounded-full text-secondary-700 dark:text-secondary-200 bg-white dark:bg-secondary-700 hover:bg-secondary-50 dark:hover:bg-secondary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-colors">
|
class="text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 font-medium rounded-lg text-sm px-3 py-1.5 inline-flex items-center border border-gray-300 dark:border-gray-600 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 transition-colors">
|
||||||
<i class="fas fa-eye mr-1"></i>
|
<i class="fas fa-eye mr-1.5"></i>
|
||||||
View Details
|
View Details
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 sm:flex sm:justify-between">
|
<div class="mt-2 sm:flex sm:justify-between">
|
||||||
<div class="sm:flex">
|
<div class="sm:flex">
|
||||||
<p class="flex items-center text-sm text-secondary-500 dark:text-secondary-400">
|
<p class="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
<i class="fas fa-calendar-alt flex-shrink-0 mr-1.5 h-5 w-5 text-secondary-400 dark:text-secondary-500"></i>
|
<i class="fas fa-calendar-alt mr-1.5 h-5 w-5 text-gray-400 dark:text-gray-500"></i>
|
||||||
Started: { history.StartTime.Format("Jan 02, 2006 15:04:05") }
|
Started: { history.StartTime.Format("Jan 02, 2006 15:04:05") }
|
||||||
</p>
|
</p>
|
||||||
if history.EndTime != nil {
|
if history.EndTime != nil {
|
||||||
<p class="mt-2 flex items-center text-sm text-secondary-500 dark:text-secondary-400 sm:mt-0 sm:ml-6">
|
<p class="mt-2 flex items-center text-sm text-gray-500 dark:text-gray-400 sm:mt-0 sm:ml-6">
|
||||||
<i class="fas fa-clock flex-shrink-0 mr-1.5 h-5 w-5 text-secondary-400 dark:text-secondary-500"></i>
|
<i class="fas fa-clock mr-1.5 h-5 w-5 text-gray-400 dark:text-gray-500"></i>
|
||||||
Duration: { history.EndTime.Sub(history.StartTime).String() }
|
Duration: { history.EndTime.Sub(history.StartTime).String() }
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 flex items-center text-sm text-secondary-500 dark:text-secondary-400 sm:mt-0">
|
<div class="mt-2 flex items-center text-sm text-gray-500 dark:text-gray-400 sm:mt-0">
|
||||||
if history.BytesTransferred > 0 {
|
if history.BytesTransferred > 0 {
|
||||||
<i class="fas fa-upload flex-shrink-0 mr-1.5 h-5 w-5 text-secondary-400 dark:text-secondary-500"></i>
|
<i class="fas fa-upload mr-1.5 h-5 w-5 text-gray-400 dark:text-gray-500"></i>
|
||||||
<p>
|
<p>
|
||||||
{ formatBytes(history.BytesTransferred) } transferred
|
{ formatBytes(history.BytesTransferred) } transferred
|
||||||
</p>
|
</p>
|
||||||
@@ -116,9 +138,9 @@ templ HistoryContent(ctx context.Context, data HistoryData) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
if history.ErrorMessage != "" {
|
if history.ErrorMessage != "" {
|
||||||
<div class="mt-2 p-2 bg-red-50 dark:bg-red-900/20 rounded border border-red-200 dark:border-red-800">
|
<div class="mt-2 p-3 rounded-lg border bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800">
|
||||||
<p class="text-sm text-red-600 dark:text-red-400">
|
<p class="text-sm text-red-600 dark:text-red-400">
|
||||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
<i class="fas fa-exclamation-triangle mr-1.5"></i>
|
||||||
Error: { history.ErrorMessage }
|
Error: { history.ErrorMessage }
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,15 +153,17 @@ templ HistoryContent(ctx context.Context, data HistoryData) {
|
|||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
if data.TotalPages > 1 {
|
if data.TotalPages > 1 {
|
||||||
<div class="mt-6 flex flex-col sm:flex-row justify-between items-center py-4 bg-secondary-50 dark:bg-secondary-800 rounded-lg">
|
<div class="mt-6 flex flex-col sm:flex-row justify-between items-center py-4 bg-white dark:bg-gray-800 rounded-lg shadow px-4">
|
||||||
<div class="flex-1 flex justify-between gap-4 sm:hidden w-full px-4 mb-4 sm:mb-0">
|
<!-- Mobile pagination -->
|
||||||
|
<div class="flex-1 flex justify-between gap-4 sm:hidden w-full mb-4 sm:mb-0">
|
||||||
if data.CurrentPage > 1 {
|
if data.CurrentPage > 1 {
|
||||||
<button
|
<button
|
||||||
hx-get="/history"
|
hx-get="/history"
|
||||||
hx-target="#history-content"
|
hx-target="#history-content"
|
||||||
hx-vals={ fmt.Sprintf(`{"page": %d, "pageSize": %d, "search": "%s"}`, data.CurrentPage-1, data.PageSize, data.SearchTerm) }
|
hx-vals={ fmt.Sprintf(`{"page": %d, "pageSize": %d, "search": "%s"}`, data.CurrentPage-1, data.PageSize, data.SearchTerm) }
|
||||||
hx-indicator="#mobile-prev-indicator"
|
hx-indicator="#mobile-prev-indicator"
|
||||||
class="flex-1 h-10 flex items-center justify-center bg-secondary-200 text-secondary-800 hover:bg-secondary-300 rounded dark:bg-secondary-700 dark:text-secondary-200 dark:hover:bg-secondary-600 relative">
|
class="w-full inline-flex justify-center items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700 relative"
|
||||||
|
>
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<i class="fas fa-chevron-left mr-1"></i> Previous
|
<i class="fas fa-chevron-left mr-1"></i> Previous
|
||||||
</span>
|
</span>
|
||||||
@@ -148,7 +172,7 @@ templ HistoryContent(ctx context.Context, data HistoryData) {
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
} else {
|
} else {
|
||||||
<span class="flex-1 h-10 flex items-center justify-center bg-secondary-100 text-secondary-300 rounded dark:bg-secondary-800 dark:text-secondary-600 cursor-not-allowed">
|
<span class="w-full inline-flex justify-center items-center px-4 py-2 text-sm font-medium text-gray-400 bg-gray-100 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-600 dark:text-gray-600 cursor-not-allowed">
|
||||||
<i class="fas fa-chevron-left mr-1"></i> Previous
|
<i class="fas fa-chevron-left mr-1"></i> Previous
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -159,7 +183,8 @@ templ HistoryContent(ctx context.Context, data HistoryData) {
|
|||||||
hx-target="#history-content"
|
hx-target="#history-content"
|
||||||
hx-vals={ fmt.Sprintf(`{"page": %d, "pageSize": %d, "search": "%s"}`, data.CurrentPage+1, data.PageSize, data.SearchTerm) }
|
hx-vals={ fmt.Sprintf(`{"page": %d, "pageSize": %d, "search": "%s"}`, data.CurrentPage+1, data.PageSize, data.SearchTerm) }
|
||||||
hx-indicator="#mobile-next-indicator"
|
hx-indicator="#mobile-next-indicator"
|
||||||
class="flex-1 h-10 flex items-center justify-center bg-secondary-200 text-secondary-800 hover:bg-secondary-300 rounded dark:bg-secondary-700 dark:text-secondary-200 dark:hover:bg-secondary-600 relative">
|
class="w-full inline-flex justify-center items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700 relative"
|
||||||
|
>
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
Next <i class="fas fa-chevron-right ml-1"></i>
|
Next <i class="fas fa-chevron-right ml-1"></i>
|
||||||
</span>
|
</span>
|
||||||
@@ -168,14 +193,16 @@ templ HistoryContent(ctx context.Context, data HistoryData) {
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
} else {
|
} else {
|
||||||
<span class="flex-1 h-10 flex items-center justify-center bg-secondary-100 text-secondary-300 rounded dark:bg-secondary-800 dark:text-secondary-600 cursor-not-allowed">
|
<span class="w-full inline-flex justify-center items-center px-4 py-2 text-sm font-medium text-gray-400 bg-gray-100 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-600 dark:text-gray-600 cursor-not-allowed">
|
||||||
Next <i class="fas fa-chevron-right ml-1"></i>
|
Next <i class="fas fa-chevron-right ml-1"></i>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between px-4">
|
|
||||||
|
<!-- Desktop pagination -->
|
||||||
|
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-secondary-700 dark:text-secondary-300">
|
<p class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
Showing
|
Showing
|
||||||
<span class="font-medium">{ fmt.Sprint((data.CurrentPage-1)*data.PageSize + 1) }</span>
|
<span class="font-medium">{ fmt.Sprint((data.CurrentPage-1)*data.PageSize + 1) }</span>
|
||||||
to
|
to
|
||||||
@@ -185,23 +212,24 @@ templ HistoryContent(ctx context.Context, data HistoryData) {
|
|||||||
results
|
results
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div>
|
||||||
<nav class="flex gap-2" aria-label="Pagination">
|
<nav class="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
||||||
if data.CurrentPage > 1 {
|
if data.CurrentPage > 1 {
|
||||||
<button
|
<button
|
||||||
hx-get="/history"
|
hx-get="/history"
|
||||||
hx-target="#history-content"
|
hx-target="#history-content"
|
||||||
hx-vals={ fmt.Sprintf(`{"page": %d, "pageSize": %d, "search": "%s"}`, data.CurrentPage-1, data.PageSize, data.SearchTerm) }
|
hx-vals={ fmt.Sprintf(`{"page": %d, "pageSize": %d, "search": "%s"}`, data.CurrentPage-1, data.PageSize, data.SearchTerm) }
|
||||||
hx-indicator="#prev-indicator"
|
hx-indicator="#prev-indicator"
|
||||||
class="w-10 h-10 flex items-center justify-center bg-secondary-200 text-secondary-800 hover:bg-secondary-300 rounded dark:bg-secondary-700 dark:text-secondary-200 dark:hover:bg-secondary-600 relative">
|
class="relative inline-flex items-center justify-center rounded-l-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700"
|
||||||
<i class="fas fa-chevron-left"></i>
|
>
|
||||||
|
<i class="fas fa-chevron-left text-xs"></i>
|
||||||
<span id="prev-indicator" class="htmx-indicator absolute top-0 right-0 w-4 h-4 -mt-1 -mr-1">
|
<span id="prev-indicator" class="htmx-indicator absolute top-0 right-0 w-4 h-4 -mt-1 -mr-1">
|
||||||
<i class="fas fa-spinner fa-spin text-xs"></i>
|
<i class="fas fa-spinner fa-spin text-xs"></i>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
} else {
|
} else {
|
||||||
<span class="w-10 h-10 flex items-center justify-center bg-secondary-100 text-secondary-300 rounded dark:bg-secondary-800 dark:text-secondary-600 cursor-not-allowed">
|
<span class="relative inline-flex items-center justify-center rounded-l-lg border border-gray-300 bg-gray-100 px-3 py-2 text-sm font-medium text-gray-400 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-600 cursor-not-allowed">
|
||||||
<i class="fas fa-chevron-left"></i>
|
<i class="fas fa-chevron-left text-xs"></i>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,15 +242,16 @@ templ HistoryContent(ctx context.Context, data HistoryData) {
|
|||||||
hx-target="#history-content"
|
hx-target="#history-content"
|
||||||
hx-vals={ fmt.Sprintf(`{"page": %d, "pageSize": %d, "search": "%s"}`, data.CurrentPage+1, data.PageSize, data.SearchTerm) }
|
hx-vals={ fmt.Sprintf(`{"page": %d, "pageSize": %d, "search": "%s"}`, data.CurrentPage+1, data.PageSize, data.SearchTerm) }
|
||||||
hx-indicator="#next-indicator"
|
hx-indicator="#next-indicator"
|
||||||
class="w-10 h-10 flex items-center justify-center bg-secondary-200 text-secondary-800 hover:bg-secondary-300 rounded dark:bg-secondary-700 dark:text-secondary-200 dark:hover:bg-secondary-600 relative">
|
class="relative inline-flex items-center justify-center rounded-r-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700"
|
||||||
<i class="fas fa-chevron-right"></i>
|
>
|
||||||
|
<i class="fas fa-chevron-right text-xs"></i>
|
||||||
<span id="next-indicator" class="htmx-indicator absolute top-0 right-0 w-4 h-4 -mt-1 -mr-1">
|
<span id="next-indicator" class="htmx-indicator absolute top-0 right-0 w-4 h-4 -mt-1 -mr-1">
|
||||||
<i class="fas fa-spinner fa-spin text-xs"></i>
|
<i class="fas fa-spinner fa-spin text-xs"></i>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
} else {
|
} else {
|
||||||
<span class="w-10 h-10 flex items-center justify-center bg-secondary-100 text-secondary-300 rounded dark:bg-secondary-800 dark:text-secondary-600 cursor-not-allowed">
|
<span class="relative inline-flex items-center justify-center rounded-r-lg border border-gray-300 bg-gray-100 px-3 py-2 text-sm font-medium text-gray-400 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-600 cursor-not-allowed">
|
||||||
<i class="fas fa-chevron-right"></i>
|
<i class="fas fa-chevron-right text-xs"></i>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</nav>
|
</nav>
|
||||||
@@ -235,42 +264,45 @@ templ HistoryContent(ctx context.Context, data HistoryData) {
|
|||||||
|
|
||||||
templ History(ctx context.Context, data HistoryData) {
|
templ History(ctx context.Context, data HistoryData) {
|
||||||
@LayoutWithContext("Transfer History", ctx) {
|
@LayoutWithContext("Transfer History", ctx) {
|
||||||
<div id="history-page" class="py-6">
|
<div id="history-container" style="min-height: 100vh;" class="bg-gray-50 dark:bg-gray-900">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="pb-8 w-full">
|
||||||
<div class="flex items-center justify-between mb-8">
|
<!-- Page Header -->
|
||||||
<h1 class="text-3xl font-bold text-secondary-900 dark:text-secondary-100">
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
<i class="fas fa-history mr-2 text-primary-600 dark:text-primary-400"></i>
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-history w-6 h-6 mr-2 text-blue-500"></i>
|
||||||
Transfer History
|
Transfer History
|
||||||
</h1>
|
</h1>
|
||||||
<div class="text-sm text-secondary-500 dark:text-secondary-400">
|
<div class="text-sm text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800 rounded-lg px-4 py-2 inline-flex items-center">
|
||||||
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
<span class="font-medium">Total: { fmt.Sprint(data.Total) } transfers</span>
|
<span class="font-medium">Total: { fmt.Sprint(data.Total) } transfers</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Search and Pagination Controls -->
|
<!-- Search and Pagination Controls -->
|
||||||
<div class="mt-4 flex flex-col sm:flex-row justify-between items-center bg-secondary-50 dark:bg-secondary-800 p-4 rounded-lg mb-6">
|
<div class="mb-6 p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
||||||
<form hx-get="/history" hx-target="#history-content" hx-indicator="#search-indicator" class="w-full sm:w-auto mb-4 sm:mb-0">
|
<form hx-get="/history" hx-target="#history-content" hx-indicator="#search-indicator" class="flex flex-col sm:flex-row gap-4 items-start sm:items-center">
|
||||||
<div class="flex">
|
<div class="w-full sm:w-auto relative flex-grow">
|
||||||
<div class="relative flex-grow">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<i class="fas fa-search text-gray-400 dark:text-gray-500"></i>
|
||||||
<i class="fas fa-search text-secondary-400 dark:text-secondary-500"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="search"
|
|
||||||
value={ data.SearchTerm }
|
|
||||||
placeholder="Search by job name or status..."
|
|
||||||
class="block w-full pl-10 pr-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md leading-5 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-secondary-100 placeholder-secondary-500 dark:placeholder-secondary-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
|
||||||
/>
|
|
||||||
<input type="hidden" name="page" value="1" />
|
|
||||||
<input type="hidden" name="pageSize" value={ fmt.Sprint(data.PageSize) } />
|
|
||||||
</div>
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="search"
|
||||||
|
value={ data.SearchTerm }
|
||||||
|
placeholder="Search by job name or status..."
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 pr-3 py-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
<input type="hidden" name="page" value="1" />
|
||||||
|
<input type="hidden" name="pageSize" value={ fmt.Sprint(data.PageSize) } />
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="ml-3 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 dark:bg-primary-500 dark:hover:bg-primary-600 transition-colors"
|
class="inline-flex items-center py-2.5 px-4 text-sm font-medium text-white bg-blue-700 rounded-lg border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
||||||
>
|
>
|
||||||
<span>Search</span>
|
<i class="fas fa-search mr-2 -ml-1"></i>
|
||||||
<span id="search-indicator" class="htmx-indicator ml-1">
|
Search
|
||||||
|
<span id="search-indicator" class="htmx-indicator ml-2">
|
||||||
<i class="fas fa-spinner fa-spin"></i>
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -280,46 +312,65 @@ templ History(ctx context.Context, data HistoryData) {
|
|||||||
hx-target="#history-content"
|
hx-target="#history-content"
|
||||||
hx-indicator="#clear-indicator"
|
hx-indicator="#clear-indicator"
|
||||||
hx-vals={ fmt.Sprintf(`{"pageSize": %d}`, data.PageSize) }
|
hx-vals={ fmt.Sprintf(`{"pageSize": %d}`, data.PageSize) }
|
||||||
class="ml-2 inline-flex items-center px-4 py-2 border border-secondary-300 dark:border-secondary-600 text-sm font-medium rounded-md text-secondary-700 dark:text-secondary-200 bg-white dark:bg-secondary-700 hover:bg-secondary-50 dark:hover:bg-secondary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-colors"
|
class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 inline-flex items-center"
|
||||||
>
|
>
|
||||||
<i class="fas fa-times mr-1"></i>
|
<i class="fas fa-times mr-2"></i>
|
||||||
Clear
|
Clear
|
||||||
<span id="clear-indicator" class="htmx-indicator ml-1">
|
<span id="clear-indicator" class="htmx-indicator ml-2">
|
||||||
<i class="fas fa-spinner fa-spin"></i>
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ml-auto">
|
||||||
|
<label class="inline-flex items-center text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
Show
|
||||||
|
<select
|
||||||
|
name="pageSize"
|
||||||
|
hx-get="/history"
|
||||||
|
hx-target="#history-content"
|
||||||
|
hx-include="[name='search']"
|
||||||
|
hx-indicator="#size-indicator"
|
||||||
|
class="mx-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
>
|
||||||
|
<option value="10" selected?={ data.PageSize == 10 }>10</option>
|
||||||
|
<option value="25" selected?={ data.PageSize == 25 }>25</option>
|
||||||
|
<option value="50" selected?={ data.PageSize == 50 }>50</option>
|
||||||
|
<option value="100" selected?={ data.PageSize == 100 }>100</option>
|
||||||
|
</select>
|
||||||
|
entries
|
||||||
|
<span id="size-indicator" class="htmx-indicator ml-2">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="text-sm text-secondary-700 dark:text-secondary-300">
|
|
||||||
Show
|
|
||||||
<select
|
|
||||||
name="pageSize"
|
|
||||||
hx-get="/history"
|
|
||||||
hx-target="#history-content"
|
|
||||||
hx-include="[name='search']"
|
|
||||||
hx-indicator="#size-indicator"
|
|
||||||
class="mx-1 rounded-md border-secondary-300 dark:border-secondary-600 py-1 text-base bg-white dark:bg-secondary-700 text-secondary-900 dark:text-secondary-100 focus:border-primary-500 focus:outline-none focus:ring-primary-500 sm:text-sm"
|
|
||||||
>
|
|
||||||
<option value="10" selected?={ data.PageSize == 10 }>10</option>
|
|
||||||
<option value="25" selected?={ data.PageSize == 25 }>25</option>
|
|
||||||
<option value="50" selected?={ data.PageSize == 50 }>50</option>
|
|
||||||
<option value="100" selected?={ data.PageSize == 100 }>100</option>
|
|
||||||
</select>
|
|
||||||
entries
|
|
||||||
<span id="size-indicator" class="htmx-indicator ml-1">
|
|
||||||
<i class="fas fa-spinner fa-spin"></i>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="history-content" class="mt-6">
|
<div id="history-content" class="mt-6">
|
||||||
@HistoryContent(ctx, data)
|
@HistoryContent(ctx, data)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Set dark background color if in dark mode
|
||||||
|
if (document.documentElement.classList.contains('dark')) {
|
||||||
|
document.getElementById('history-container').style.backgroundColor = '#111827';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener for theme changes
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener('click', function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
document.getElementById('history-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -328,7 +379,7 @@ templ pageNumbers(currentPage int, totalPages int, pageSize int, searchTerm stri
|
|||||||
// Show at most 5 page numbers with the current page in the middle when possible
|
// Show at most 5 page numbers with the current page in the middle when possible
|
||||||
for i := 1; i <= totalPages; i++ {
|
for i := 1; i <= totalPages; i++ {
|
||||||
if i == currentPage {
|
if i == currentPage {
|
||||||
<span aria-current="page" class="w-10 h-10 flex items-center justify-center bg-primary-600 text-white rounded">
|
<span aria-current="page" class="relative z-10 inline-flex items-center bg-blue-600 px-4 py-2 text-sm font-medium text-white focus:z-20 border border-blue-600">
|
||||||
{ fmt.Sprint(i) }
|
{ fmt.Sprint(i) }
|
||||||
</span>
|
</span>
|
||||||
} else if i == 1 || i == totalPages || (i >= currentPage-2 && i <= currentPage+2) {
|
} else if i == 1 || i == totalPages || (i >= currentPage-2 && i <= currentPage+2) {
|
||||||
@@ -337,14 +388,17 @@ templ pageNumbers(currentPage int, totalPages int, pageSize int, searchTerm stri
|
|||||||
hx-target="#history-content"
|
hx-target="#history-content"
|
||||||
hx-vals={ fmt.Sprintf(`{"page": %d, "pageSize": %d, "search": "%s"}`, i, pageSize, searchTerm) }
|
hx-vals={ fmt.Sprintf(`{"page": %d, "pageSize": %d, "search": "%s"}`, i, pageSize, searchTerm) }
|
||||||
hx-indicator={ fmt.Sprintf("#page-indicator-%d", i) }
|
hx-indicator={ fmt.Sprintf("#page-indicator-%d", i) }
|
||||||
class="w-10 h-10 flex items-center justify-center bg-secondary-200 text-secondary-800 hover:bg-secondary-300 rounded dark:bg-secondary-700 dark:text-secondary-200 dark:hover:bg-secondary-600 relative">
|
class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20 border border-gray-300 bg-white dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700 relative"
|
||||||
|
>
|
||||||
{ fmt.Sprint(i) }
|
{ fmt.Sprint(i) }
|
||||||
<span id={ fmt.Sprintf("page-indicator-%d", i) } class="htmx-indicator absolute top-0 right-0 w-4 h-4 -mt-1 -mr-1">
|
<span id={ fmt.Sprintf("page-indicator-%d", i) } class="htmx-indicator absolute top-0 right-0 w-4 h-4 -mt-1 -mr-1">
|
||||||
<i class="fas fa-spinner fa-spin text-xs"></i>
|
<i class="fas fa-spinner fa-spin text-xs"></i>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
} else if i == currentPage-3 || i == currentPage+3 {
|
} else if i == currentPage-3 || i == currentPage+3 {
|
||||||
<span class="w-10 h-10 flex items-center justify-center">...</span>
|
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 bg-white dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400">
|
||||||
|
...
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+124
-99
@@ -6,130 +6,155 @@ import (
|
|||||||
|
|
||||||
templ Home(ctx context.Context) {
|
templ Home(ctx context.Context) {
|
||||||
@LayoutWithContext("Home", ctx) {
|
@LayoutWithContext("Home", ctx) {
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
<div class="p-4 md:p-6 2xl:p-10">
|
||||||
<div class="text-center">
|
<!-- Page Header -->
|
||||||
<h1 class="text-4xl font-bold text-secondary-900 dark:text-secondary-100 mb-4">
|
<div class="mb-8 text-center">
|
||||||
<i class="fas fa-exchange-alt text-primary-600 dark:text-primary-400 mr-3"></i>
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white flex items-center justify-center">
|
||||||
|
<i class="fas fa-exchange-alt w-8 h-8 mr-3 text-blue-500"></i>
|
||||||
Welcome to GoMFT
|
Welcome to GoMFT
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-xl text-secondary-600 dark:text-secondary-300 mb-12">A modern managed file transfer solution</p>
|
<p class="text-lg text-gray-500 dark:text-gray-400 mt-2">
|
||||||
|
A modern managed file transfer solution
|
||||||
if isLoggedIn(ctx) {
|
</p>
|
||||||
<div class="space-y-8">
|
</div>
|
||||||
<!-- Feature Cards -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
if isLoggedIn(ctx) {
|
||||||
<div class="card hover:shadow-lg transition-shadow duration-300">
|
<!-- Logged-in User View -->
|
||||||
<div class="p-6">
|
<div class="space-y-8">
|
||||||
<div class="rounded-full bg-primary-100 dark:bg-primary-900 p-4 w-16 h-16 flex items-center justify-center mb-4 mx-auto">
|
<!-- Feature Cards -->
|
||||||
<i class="fas fa-cogs text-primary-600 dark:text-primary-300 text-2xl"></i>
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
</div>
|
<!-- Transfer Configs Card -->
|
||||||
<h2 class="text-2xl font-semibold text-secondary-900 dark:text-secondary-100 mb-4 text-center">Transfer Configs</h2>
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 hover:shadow-lg transition-shadow duration-300">
|
||||||
<p class="text-secondary-600 dark:text-secondary-400 mb-6 text-center">Set up and manage file transfer configurations with ease.</p>
|
<div class="p-6 text-center">
|
||||||
<div class="flex justify-center">
|
<div class="inline-flex h-14 w-14 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 mb-4 dark:bg-blue-900">
|
||||||
<a href="/configs" class="btn-primary inline-flex items-center">
|
<i class="fas fa-cogs text-blue-600 dark:text-blue-300 text-2xl"></i>
|
||||||
<span>View Configs</span>
|
|
||||||
<i class="fas fa-arrow-right ml-2"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card hover:shadow-lg transition-shadow duration-300">
|
|
||||||
<div class="p-6">
|
|
||||||
<div class="rounded-full bg-blue-100 dark:bg-blue-900 p-4 w-16 h-16 flex items-center justify-center mb-4 mx-auto">
|
|
||||||
<i class="fas fa-calendar-alt text-blue-600 dark:text-blue-300 text-2xl"></i>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-2xl font-semibold text-secondary-900 dark:text-secondary-100 mb-4 text-center">Schedule Jobs</h2>
|
|
||||||
<p class="text-secondary-600 dark:text-secondary-400 mb-6 text-center">Create and monitor automated file transfer jobs.</p>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<a href="/jobs" class="btn-primary inline-flex items-center">
|
|
||||||
<span>View Jobs</span>
|
|
||||||
<i class="fas fa-arrow-right ml-2"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card hover:shadow-lg transition-shadow duration-300">
|
|
||||||
<div class="p-6">
|
|
||||||
<div class="rounded-full bg-green-100 dark:bg-green-900 p-4 w-16 h-16 flex items-center justify-center mb-4 mx-auto">
|
|
||||||
<i class="fas fa-history text-green-600 dark:text-green-300 text-2xl"></i>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-2xl font-semibold text-secondary-900 dark:text-secondary-100 mb-4 text-center">Track History</h2>
|
|
||||||
<p class="text-secondary-600 dark:text-secondary-400 mb-6 text-center">View detailed history of all file transfer operations.</p>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<a href="/history" class="btn-primary inline-flex items-center">
|
|
||||||
<span>View History</span>
|
|
||||||
<i class="fas fa-arrow-right ml-2"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-3">Transfer Configs</h2>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 mb-5">Set up and manage file transfer configurations with ease.</p>
|
||||||
|
<a href="/configs" class="inline-flex items-center px-4 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
|
View Configs
|
||||||
|
<i class="fas fa-arrow-right ml-2"></i>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick Access Section -->
|
<!-- Schedule Jobs Card -->
|
||||||
<div class="mt-12">
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 hover:shadow-lg transition-shadow duration-300">
|
||||||
<h2 class="text-2xl font-bold text-secondary-900 dark:text-secondary-100 mb-6">
|
<div class="p-6 text-center">
|
||||||
<i class="fas fa-bolt text-primary-600 dark:text-primary-400 mr-2"></i>
|
<div class="inline-flex h-14 w-14 flex-shrink-0 items-center justify-center rounded-full bg-indigo-100 mb-4 dark:bg-indigo-900">
|
||||||
Quick Access
|
<i class="fas fa-calendar-alt text-indigo-600 dark:text-indigo-300 text-2xl"></i>
|
||||||
</h2>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-3">Schedule Jobs</h2>
|
||||||
<a href="/dashboard" class="card p-4 hover:bg-secondary-50 dark:hover:bg-secondary-800 flex items-center transition-colors">
|
<p class="text-gray-500 dark:text-gray-400 mb-5">Create and monitor automated file transfer jobs.</p>
|
||||||
<i class="fas fa-tachometer-alt text-primary-600 dark:text-primary-400 text-xl mr-3"></i>
|
<a href="/jobs" class="inline-flex items-center px-4 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
<span class="text-secondary-900 dark:text-secondary-100">Dashboard</span>
|
View Jobs
|
||||||
|
<i class="fas fa-arrow-right ml-2"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="/configs/new" class="card p-4 hover:bg-secondary-50 dark:hover:bg-secondary-800 flex items-center transition-colors">
|
</div>
|
||||||
<i class="fas fa-plus-circle text-primary-600 dark:text-primary-400 text-xl mr-3"></i>
|
</div>
|
||||||
<span class="text-secondary-900 dark:text-secondary-100">New Config</span>
|
|
||||||
</a>
|
<!-- Track History Card -->
|
||||||
<a href="/jobs/new" class="card p-4 hover:bg-secondary-50 dark:hover:bg-secondary-800 flex items-center transition-colors">
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 hover:shadow-lg transition-shadow duration-300">
|
||||||
<i class="fas fa-play-circle text-primary-600 dark:text-primary-400 text-xl mr-3"></i>
|
<div class="p-6 text-center">
|
||||||
<span class="text-secondary-900 dark:text-secondary-100">New Job</span>
|
<div class="inline-flex h-14 w-14 flex-shrink-0 items-center justify-center rounded-full bg-green-100 mb-4 dark:bg-green-900">
|
||||||
</a>
|
<i class="fas fa-history text-green-600 dark:text-green-300 text-2xl"></i>
|
||||||
<a href="/profile" class="card p-4 hover:bg-secondary-50 dark:hover:bg-secondary-800 flex items-center transition-colors">
|
</div>
|
||||||
<i class="fas fa-user-circle text-primary-600 dark:text-primary-400 text-xl mr-3"></i>
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-3">Track History</h2>
|
||||||
<span class="text-secondary-900 dark:text-secondary-100">Profile</span>
|
<p class="text-gray-500 dark:text-gray-400 mb-5">View detailed history of all file transfer operations.</p>
|
||||||
|
<a href="/history" class="inline-flex items-center px-4 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
|
View History
|
||||||
|
<i class="fas fa-arrow-right ml-2"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} else {
|
|
||||||
<div class="max-w-md mx-auto card p-8 hover:shadow-lg transition-shadow duration-300">
|
<!-- Quick Access Section -->
|
||||||
<div class="inline-flex items-center justify-center w-64 h-64 mb-4">
|
<div class="mt-10">
|
||||||
<img src="/static/img/logo.svg" alt="GoMFT Logo" class="w-full h-full"/>
|
<div class="flex items-center mb-4">
|
||||||
|
<i class="fas fa-bolt text-blue-500 w-6 h-6 mr-2"></i>
|
||||||
|
<h2 class="text-xl font-bold text-gray-900 dark:text-white">Quick Access</h2>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<a href="/dashboard" class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 p-4 flex items-center transition-colors hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
|
<div class="inline-flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 mr-3 dark:bg-blue-900">
|
||||||
|
<i class="fas fa-tachometer-alt text-blue-600 dark:text-blue-300"></i>
|
||||||
|
</div>
|
||||||
|
<span class="font-medium text-gray-900 dark:text-white">Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<a href="/configs/new" class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 p-4 flex items-center transition-colors hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
|
<div class="inline-flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 mr-3 dark:bg-blue-900">
|
||||||
|
<i class="fas fa-plus-circle text-blue-600 dark:text-blue-300"></i>
|
||||||
|
</div>
|
||||||
|
<span class="font-medium text-gray-900 dark:text-white">New Config</span>
|
||||||
|
</a>
|
||||||
|
<a href="/jobs/new" class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 p-4 flex items-center transition-colors hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
|
<div class="inline-flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 mr-3 dark:bg-blue-900">
|
||||||
|
<i class="fas fa-play-circle text-blue-600 dark:text-blue-300"></i>
|
||||||
|
</div>
|
||||||
|
<span class="font-medium text-gray-900 dark:text-white">New Job</span>
|
||||||
|
</a>
|
||||||
|
<a href="/profile" class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 p-4 flex items-center transition-colors hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
|
<div class="inline-flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 mr-3 dark:bg-blue-900">
|
||||||
|
<i class="fas fa-user-circle text-blue-600 dark:text-blue-300"></i>
|
||||||
|
</div>
|
||||||
|
<span class="font-medium text-gray-900 dark:text-white">Profile</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<!-- Login Card for Non-Logged-In Users -->
|
||||||
|
<div class="max-w-md mx-auto">
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
<div class="p-6 text-center">
|
||||||
|
<div class="inline-flex items-center justify-center w-32 h-32 mb-6">
|
||||||
|
<img src="/static/img/logo.svg" alt="GoMFT Logo" class="w-full h-full"/>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-3">Get Started</h2>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 mb-6">Log in to access the file transfer management system.</p>
|
||||||
|
<a href="/login" class="inline-flex items-center px-5 py-2.5 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-lock mr-2"></i>
|
||||||
|
Log In
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-2xl font-semibold text-secondary-900 dark:text-secondary-100 mb-4">Get Started</h2>
|
|
||||||
<p class="text-secondary-600 dark:text-secondary-400 mb-6">Log in to access the file transfer management system.</p>
|
|
||||||
<a href="/login" class="btn-primary inline-flex items-center px-8 py-3">
|
|
||||||
<i class="fas fa-lock mr-2"></i>
|
|
||||||
<span>Log In</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<!-- Features Preview -->
|
<!-- Features Preview -->
|
||||||
<div class="mt-12 pt-8 border-t border-secondary-200 dark:border-secondary-700">
|
<div class="p-6 bg-gray-50 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 rounded-b-lg">
|
||||||
<h3 class="text-lg font-medium text-secondary-900 dark:text-secondary-100 mb-4">Key Features</h3>
|
<h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">Key Features</h3>
|
||||||
<ul class="space-y-3 text-left">
|
<ul class="space-y-3">
|
||||||
<li class="flex items-start">
|
<li class="flex items-start">
|
||||||
<i class="fas fa-check-circle text-green-500 mt-1 mr-2"></i>
|
<i class="fas fa-check-circle text-green-500 mt-0.5 mr-2 flex-shrink-0"></i>
|
||||||
<span class="text-secondary-600 dark:text-secondary-400">Secure file transfers with encryption</span>
|
<span class="text-gray-500 dark:text-gray-400">Secure file transfers with encryption</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex items-start">
|
<li class="flex items-start">
|
||||||
<i class="fas fa-check-circle text-green-500 mt-1 mr-2"></i>
|
<i class="fas fa-check-circle text-green-500 mt-0.5 mr-2 flex-shrink-0"></i>
|
||||||
<span class="text-secondary-600 dark:text-secondary-400">Automated scheduling and monitoring</span>
|
<span class="text-gray-500 dark:text-gray-400">Automated scheduling and monitoring</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex items-start">
|
<li class="flex items-start">
|
||||||
<i class="fas fa-check-circle text-green-500 mt-1 mr-2"></i>
|
<i class="fas fa-check-circle text-green-500 mt-0.5 mr-2 flex-shrink-0"></i>
|
||||||
<span class="text-secondary-600 dark:text-secondary-400">Comprehensive transfer history tracking</span>
|
<span class="text-gray-500 dark:text-gray-400">Comprehensive transfer history tracking</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex items-start">
|
<li class="flex items-start">
|
||||||
<i class="fas fa-check-circle text-green-500 mt-1 mr-2"></i>
|
<i class="fas fa-check-circle text-green-500 mt-0.5 mr-2 flex-shrink-0"></i>
|
||||||
<span class="text-secondary-600 dark:text-secondary-400">User-friendly interface with dark mode support</span>
|
<span class="text-gray-500 dark:text-gray-400">User-friendly interface with dark mode support</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
</div>
|
<!-- Help Notice -->
|
||||||
|
<div class="mt-6">
|
||||||
|
<div class="p-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/50 dark:text-blue-400 border border-blue-200 dark:border-blue-800 flex items-start">
|
||||||
|
<i class="fas fa-info-circle flex-shrink-0 mr-3 mt-0.5"></i>
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-medium mb-1">About GoMFT</h4>
|
||||||
|
<p>GoMFT is a modern, secure file transfer solution that allows you to effortlessly move files between different systems and storage providers with robust tracking and monitoring.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+368
-547
@@ -245,560 +245,381 @@ templ configSearchScript() {
|
|||||||
templ JobForm(ctx context.Context, data JobFormData) {
|
templ JobForm(ctx context.Context, data JobFormData) {
|
||||||
@LayoutWithContext(getJobFormTitle(data.IsNew), ctx) {
|
@LayoutWithContext(getJobFormTitle(data.IsNew), ctx) {
|
||||||
@configSearchScript()
|
@configSearchScript()
|
||||||
<div class="min-h-[calc(100vh-4rem)] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 bg-secondary-50 dark:bg-secondary-900">
|
|
||||||
<div class="max-w-3xl w-full">
|
<!-- Main Content -->
|
||||||
<div class="card overflow-hidden shadow-lg">
|
<section class="py-8 px-4">
|
||||||
<div class="p-8">
|
<div class="mx-auto max-w-3xl">
|
||||||
<div class="text-center mb-8">
|
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-6 md:p-8">
|
||||||
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-primary-100 dark:bg-primary-900 mb-4">
|
<!-- Form Header -->
|
||||||
<i class="fas fa-calendar-alt text-primary-600 dark:text-primary-400 text-3xl"></i>
|
<div class="mb-8 text-center">
|
||||||
</div>
|
<div class="flex justify-center mb-4">
|
||||||
<h2 class="text-3xl font-bold text-secondary-900 dark:text-secondary-100">
|
<span class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-50 dark:bg-blue-900">
|
||||||
{ getJobTitle(data.IsNew) }
|
<i class="fas fa-calendar-alt text-blue-600 dark:text-blue-300 text-2xl"></i>
|
||||||
</h2>
|
</span>
|
||||||
<p class="mt-2 text-secondary-600 dark:text-secondary-400">Configure your scheduled transfer job</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
if data.IsNew {
|
{ getJobTitle(data.IsNew) }
|
||||||
<form
|
</h1>
|
||||||
id="new-job-form"
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||||
class="space-y-6"
|
Configure your scheduled file transfer job using the form below
|
||||||
hx-post="/jobs"
|
|
||||||
hx-target="body"
|
|
||||||
hx-boost="true">
|
|
||||||
<div class="space-y-6">
|
|
||||||
<div>
|
|
||||||
<label for="name" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Job Name</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-tag text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="name"
|
|
||||||
id="name"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="Daily Production Backup"/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
|
||||||
Descriptive name for this job (optional). If not provided, the config name will be used.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Transfer Configurations</label>
|
|
||||||
<div class="mt-2 border border-secondary-300 dark:border-secondary-700 rounded-md overflow-hidden">
|
|
||||||
<!-- Search box -->
|
|
||||||
<div class="px-3 py-2 border-b border-secondary-200 dark:border-secondary-700 bg-secondary-50 dark:bg-secondary-800">
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-search text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="config-search"
|
|
||||||
placeholder="Search configurations..."
|
|
||||||
class="block w-full pl-10 pr-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md leading-5 bg-white dark:bg-secondary-800 text-secondary-900 dark:text-secondary-100 placeholder-secondary-500 dark:placeholder-secondary-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Configuration checkboxes -->
|
|
||||||
<div class="max-h-48 overflow-y-auto py-2 px-3 bg-white dark:bg-secondary-900 divide-y divide-secondary-200 dark:divide-secondary-700" id="config-list">
|
|
||||||
if len(data.Configs) > 0 {
|
|
||||||
for _, config := range data.Configs {
|
|
||||||
<div class="config-item py-2 flex items-center" data-name={ config.Name }>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="config_ids[]"
|
|
||||||
id={ fmt.Sprintf("config_%d", config.ID) }
|
|
||||||
value={ fmt.Sprint(config.ID) }
|
|
||||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 dark:border-secondary-700 rounded"
|
|
||||||
/>
|
|
||||||
<label for={ fmt.Sprintf("config_%d", config.ID) } class="ml-3 block font-medium text-secondary-700 dark:text-secondary-300 w-full cursor-pointer">
|
|
||||||
{ config.Name }
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
<div class="text-center py-4 text-secondary-500 dark:text-secondary-400">
|
|
||||||
No configurations available. <a href="/configs/new" class="text-primary-600 hover:text-primary-500">Create one</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Selected Configurations Order List -->
|
|
||||||
<div class="mt-4">
|
|
||||||
<label class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-2">
|
|
||||||
<i class="fas fa-sort-amount-down mr-1"></i> Execution Order
|
|
||||||
</label>
|
|
||||||
<div id="selected-configs" class="border border-secondary-300 dark:border-secondary-700 rounded-md p-3 min-h-20 bg-secondary-50 dark:bg-secondary-900">
|
|
||||||
<!-- Selected items will be populated by JavaScript -->
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
|
||||||
Use the arrows to change the order in which configurations will execute.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="schedule" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Schedule (Cron Expression)</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-clock text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="schedule"
|
|
||||||
id="schedule"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="*/15 * * * *"/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
|
||||||
Use standard cron expression format. Example: */15 * * * * (every 15 minutes)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-secondary-50 dark:bg-secondary-800 p-4 rounded-lg border border-secondary-200 dark:border-secondary-700">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="enabled"
|
|
||||||
name="enabled"
|
|
||||||
value="true"
|
|
||||||
if data.Job.GetEnabled() {
|
|
||||||
checked
|
|
||||||
}
|
|
||||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 dark:border-secondary-700 rounded" />
|
|
||||||
<label for="enabled" class="ml-2 block text-sm font-medium text-secondary-700 dark:text-secondary-300">Enable this job</label>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
Jobs that are not enabled will not run automatically on schedule.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Webhook Notification Settings -->
|
|
||||||
<div class="border-t border-secondary-200 dark:border-secondary-700 pt-6 mt-6">
|
|
||||||
<h3 class="text-lg font-medium text-secondary-900 dark:text-secondary-100 mb-4">
|
|
||||||
<i class="fas fa-bell mr-2 text-primary-500"></i>
|
|
||||||
Webhook Notifications
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="webhook_enabled"
|
|
||||||
name="webhook_enabled"
|
|
||||||
value="true"
|
|
||||||
if data.Job.GetWebhookEnabled() {
|
|
||||||
checked
|
|
||||||
}
|
|
||||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 dark:border-secondary-700 rounded" />
|
|
||||||
<label for="webhook_enabled" class="ml-2 block text-sm font-medium text-secondary-700 dark:text-secondary-300">
|
|
||||||
Send webhook notification on completion
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pl-6 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label for="webhook_url" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Webhook URL</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-link text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
name="webhook_url"
|
|
||||||
id="webhook_url"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="https://example.com/webhook"/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
|
||||||
The URL where notifications will be sent when jobs run
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="webhook_secret" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
|
||||||
Webhook Secret <span class="text-secondary-500 dark:text-secondary-400">(optional)</span>
|
|
||||||
</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="webhook_secret"
|
|
||||||
id="webhook_secret"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="Secret token for signing requests"/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
|
||||||
Used to sign webhook payloads (X-Hub-Signature-256 header)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="webhook_headers" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
|
||||||
Custom Headers <span class="text-secondary-500 dark:text-secondary-400">(optional)</span>
|
|
||||||
</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-code text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="webhook_headers"
|
|
||||||
id="webhook_headers"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder='{"X-Custom-Header": "value"}'/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
|
||||||
Additional HTTP headers as JSON
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="notify_on_success"
|
|
||||||
name="notify_on_success"
|
|
||||||
value="true"
|
|
||||||
if data.Job.GetNotifyOnSuccess() {
|
|
||||||
checked
|
|
||||||
}
|
|
||||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 dark:border-secondary-700 rounded"/>
|
|
||||||
<label for="notify_on_success" class="ml-2 block text-sm font-medium text-secondary-700 dark:text-secondary-300">
|
|
||||||
Notify on successful jobs
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="notify_on_failure"
|
|
||||||
name="notify_on_failure"
|
|
||||||
value="true"
|
|
||||||
if data.Job.GetNotifyOnFailure() {
|
|
||||||
checked
|
|
||||||
}
|
|
||||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 dark:border-secondary-700 rounded"/>
|
|
||||||
<label for="notify_on_failure" class="ml-2 block text-sm font-medium text-secondary-700 dark:text-secondary-300">
|
|
||||||
Notify on failed jobs
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-5 flex justify-end space-x-3">
|
|
||||||
<a href="/jobs" class="btn-secondary flex items-center justify-center px-4 py-2">
|
|
||||||
<i class="fas fa-times mr-2"></i>
|
|
||||||
Cancel
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn-primary flex items-center justify-center px-4 py-2">
|
|
||||||
<i class="fas fa-plus mr-2"></i>
|
|
||||||
Create Job
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
} else {
|
|
||||||
<form
|
|
||||||
id="edit-job-form"
|
|
||||||
class="space-y-6"
|
|
||||||
hx-post={ fmt.Sprintf("/jobs/%d", data.Job.ID) }
|
|
||||||
hx-target="body"
|
|
||||||
hx-boost="true"
|
|
||||||
data-config-order={ data.Job.ConfigIDs }>
|
|
||||||
<div class="space-y-6">
|
|
||||||
<div>
|
|
||||||
<label for="name" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Job Name</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-tag text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="name"
|
|
||||||
id="name"
|
|
||||||
value={ data.Job.Name }
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="Daily Production Backup"/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
|
||||||
Descriptive name for this job (optional). If not provided, the config name will be used.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Transfer Configurations</label>
|
|
||||||
<div class="mt-2 border border-secondary-300 dark:border-secondary-700 rounded-md overflow-hidden">
|
|
||||||
<!-- Search box -->
|
|
||||||
<div class="px-3 py-2 border-b border-secondary-200 dark:border-secondary-700 bg-secondary-50 dark:bg-secondary-800">
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-search text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="config-search-edit"
|
|
||||||
placeholder="Search configurations..."
|
|
||||||
class="block w-full pl-10 pr-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md leading-5 bg-white dark:bg-secondary-800 text-secondary-900 dark:text-secondary-100 placeholder-secondary-500 dark:placeholder-secondary-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Configuration checkboxes -->
|
|
||||||
<div class="max-h-48 overflow-y-auto py-2 px-3 bg-white dark:bg-secondary-900 divide-y divide-secondary-200 dark:divide-secondary-700" id="config-list-edit">
|
|
||||||
if len(data.Configs) > 0 {
|
|
||||||
for _, config := range data.Configs {
|
|
||||||
<div class="config-item py-2 flex items-center" data-name={ config.Name }>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="config_ids[]"
|
|
||||||
id={ fmt.Sprintf("config_edit_%d", config.ID) }
|
|
||||||
value={ fmt.Sprint(config.ID) }
|
|
||||||
if configSelected(data.Job, config.ID) {
|
|
||||||
checked
|
|
||||||
}
|
|
||||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 dark:border-secondary-700 rounded"
|
|
||||||
/>
|
|
||||||
<label for={ fmt.Sprintf("config_edit_%d", config.ID) } class="ml-3 block font-medium text-secondary-700 dark:text-secondary-300 w-full cursor-pointer">
|
|
||||||
{ config.Name }
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
<div class="text-center py-4 text-secondary-500 dark:text-secondary-400">
|
|
||||||
No configurations available. <a href="/configs/new" class="text-primary-600 hover:text-primary-500">Create one</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Selected Configurations Order List for edit -->
|
|
||||||
<div class="mt-4">
|
|
||||||
<label class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-2">
|
|
||||||
<i class="fas fa-sort-amount-down mr-1"></i> Execution Order
|
|
||||||
</label>
|
|
||||||
<div id="selected-configs-edit" class="border border-secondary-300 dark:border-secondary-700 rounded-md p-3 min-h-20 bg-secondary-50 dark:bg-secondary-900">
|
|
||||||
<!-- Selected items will be populated by JavaScript -->
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
|
||||||
Use the arrows to change the order in which configurations will execute.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="schedule" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Schedule (Cron Expression)</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-clock text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="schedule"
|
|
||||||
id="schedule"
|
|
||||||
value={ data.Job.Schedule }
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="*/15 * * * *"/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
|
||||||
Use standard cron expression format. Example: */15 * * * * (every 15 minutes)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-secondary-50 dark:bg-secondary-800 p-4 rounded-lg border border-secondary-200 dark:border-secondary-700">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="enabled"
|
|
||||||
name="enabled"
|
|
||||||
value="true"
|
|
||||||
if data.Job.GetEnabled() {
|
|
||||||
checked
|
|
||||||
}
|
|
||||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 dark:border-secondary-700 rounded" />
|
|
||||||
<label for="enabled" class="ml-2 block text-sm font-medium text-secondary-700 dark:text-secondary-300">Enable this job</label>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
Jobs that are not enabled will not run automatically on schedule.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Webhook Notification Settings -->
|
|
||||||
<div class="border-t border-secondary-200 dark:border-secondary-700 pt-6 mt-6">
|
|
||||||
<h3 class="text-lg font-medium text-secondary-900 dark:text-secondary-100 mb-4">
|
|
||||||
<i class="fas fa-bell mr-2 text-primary-500"></i>
|
|
||||||
Webhook Notifications
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="webhook_enabled"
|
|
||||||
name="webhook_enabled"
|
|
||||||
value="true"
|
|
||||||
if data.Job.GetWebhookEnabled() {
|
|
||||||
checked
|
|
||||||
}
|
|
||||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 dark:border-secondary-700 rounded" />
|
|
||||||
<label for="webhook_enabled" class="ml-2 block text-sm font-medium text-secondary-700 dark:text-secondary-300">
|
|
||||||
Send webhook notification on completion
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pl-6 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label for="webhook_url" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Webhook URL</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-link text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
name="webhook_url"
|
|
||||||
id="webhook_url"
|
|
||||||
value={ data.Job.WebhookURL }
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="https://example.com/webhook"/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
|
||||||
The URL where notifications will be sent when jobs run
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="webhook_secret" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
|
||||||
Webhook Secret <span class="text-secondary-500 dark:text-secondary-400">(optional)</span>
|
|
||||||
</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="webhook_secret"
|
|
||||||
id="webhook_secret"
|
|
||||||
value={ data.Job.WebhookSecret }
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="Secret token for signing requests"/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
|
||||||
Used to sign webhook payloads (X-Hub-Signature-256 header)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="webhook_headers" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
|
||||||
Custom Headers <span class="text-secondary-500 dark:text-secondary-400">(optional)</span>
|
|
||||||
</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-code text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="webhook_headers"
|
|
||||||
id="webhook_headers"
|
|
||||||
value={ data.Job.WebhookHeaders }
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder='{"X-Custom-Header": "value"}'/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
|
||||||
Additional HTTP headers as JSON
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="notify_on_success"
|
|
||||||
name="notify_on_success"
|
|
||||||
value="true"
|
|
||||||
if data.Job.GetNotifyOnSuccess() {
|
|
||||||
checked
|
|
||||||
}
|
|
||||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 dark:border-secondary-700 rounded"/>
|
|
||||||
<label for="notify_on_success" class="ml-2 block text-sm font-medium text-secondary-700 dark:text-secondary-300">
|
|
||||||
Notify on successful jobs
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="notify_on_failure"
|
|
||||||
name="notify_on_failure"
|
|
||||||
value="true"
|
|
||||||
if data.Job.GetNotifyOnFailure() {
|
|
||||||
checked
|
|
||||||
}
|
|
||||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 dark:border-secondary-700 rounded"/>
|
|
||||||
<label for="notify_on_failure" class="ml-2 block text-sm font-medium text-secondary-700 dark:text-secondary-300">
|
|
||||||
Notify on failed jobs
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-5 flex justify-end space-x-3">
|
|
||||||
<a href="/jobs" class="btn-secondary flex items-center justify-center px-4 py-2">
|
|
||||||
<i class="fas fa-times mr-2"></i>
|
|
||||||
Cancel
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn-primary flex items-center justify-center px-4 py-2">
|
|
||||||
<i class="fas fa-save mr-2"></i>
|
|
||||||
Save Changes
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="px-8 py-4 bg-secondary-50 dark:bg-secondary-800 border-t border-secondary-200 dark:border-secondary-700 text-center">
|
|
||||||
<p class="text-sm text-secondary-600 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
|
||||||
Jobs will run according to their schedule and execute the selected transfer configurations in the order specified
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Job Form Information -->
|
||||||
|
<div class="p-4 mb-6 bg-blue-50 border border-blue-100 rounded-lg dark:bg-blue-900/20 dark:border-blue-900">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<i class="fas fa-info-circle text-blue-600 dark:text-blue-400 mr-2"></i>
|
||||||
|
<h3 class="text-lg font-medium text-blue-600 dark:text-blue-400">Job Information</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
Jobs run on a schedule and can include one or more transfer configurations. Select which configurations to run and in what order.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Form -->
|
||||||
|
if data.IsNew {
|
||||||
|
<form id="new-job-form" hx-post="/jobs" hx-target="body" hx-boost="true" class="space-y-6">
|
||||||
|
<!-- Job Details Section -->
|
||||||
|
<div class="p-5 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<h3 class="mb-4 text-xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-tag mr-2 text-blue-500 dark:text-blue-400"></i>Job Details
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Job name field -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
Job Name
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||||
|
<i class="fas fa-tag text-gray-500 dark:text-gray-400"></i>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
id="name"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="Daily Production Backup"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-info-circle mr-1"></i>
|
||||||
|
Descriptive name for this job (optional). If not provided, the config name will be used.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Schedule field -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="schedule" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
Schedule (Cron Expression)
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||||
|
<i class="fas fa-clock text-gray-500 dark:text-gray-400"></i>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="schedule"
|
||||||
|
id="schedule"
|
||||||
|
required
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="*/15 * * * *"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-info-circle mr-1"></i>
|
||||||
|
Use standard cron expression format. Example: */15 * * * * (every 15 minutes)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Enabled toggle -->
|
||||||
|
<div class="p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="enabled"
|
||||||
|
value="true"
|
||||||
|
id="enabled"
|
||||||
|
if data.Job.GetEnabled() {
|
||||||
|
checked
|
||||||
|
}
|
||||||
|
class="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||||
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">Enable this job</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-info-circle mr-1"></i>
|
||||||
|
Disabled jobs won't run automatically but can still be triggered manually.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Configuration Selection Section -->
|
||||||
|
<div class="p-5 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<h3 class="mb-4 text-xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-cog mr-2 text-blue-500 dark:text-blue-400"></i>Transfer Configurations
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Search field -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="config-search" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
Search Configurations
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||||
|
<i class="fas fa-search text-gray-500 dark:text-gray-400"></i>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="config-search"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="Search configurations..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Configuration list -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
Available Configurations
|
||||||
|
</label>
|
||||||
|
<div class="border border-gray-200 rounded-lg dark:border-gray-700 overflow-hidden">
|
||||||
|
<div class="max-h-56 overflow-y-auto" id="config-list">
|
||||||
|
if len(data.Configs) > 0 {
|
||||||
|
for _, config := range data.Configs {
|
||||||
|
<div class="config-item px-4 py-3 border-b last:border-b-0 border-gray-200 dark:border-gray-700 flex items-center" data-name={ config.Name }>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="config_ids[]"
|
||||||
|
id={ fmt.Sprintf("config_%d", config.ID) }
|
||||||
|
value={ fmt.Sprint(config.ID) }
|
||||||
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
<label for={ fmt.Sprintf("config_%d", config.ID) } class="ms-2 text-sm font-medium text-gray-900 dark:text-white cursor-pointer w-full">
|
||||||
|
{ config.Name }
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
<div class="text-center py-4 text-gray-500 dark:text-gray-400">
|
||||||
|
No configurations available. <a href="/configs/new" class="text-blue-600 dark:text-blue-500 hover:underline">Create one</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selected configurations order list -->
|
||||||
|
<div>
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
<i class="fas fa-sort-amount-down mr-1"></i> Execution Order
|
||||||
|
</label>
|
||||||
|
<div id="selected-configs" class="border border-gray-200 dark:border-gray-700 rounded-lg p-4 min-h-16 bg-gray-50 dark:bg-gray-800">
|
||||||
|
<!-- Items will be populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-info-circle mr-1"></i>
|
||||||
|
Use the arrows to change the order in which configurations will execute.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form actions -->
|
||||||
|
<div class="flex items-center justify-between pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<a href="/jobs" class="text-white bg-gray-500 hover:bg-gray-600 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800">
|
||||||
|
<i class="fas fa-arrow-left mr-2"></i>Cancel
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-plus mr-2"></i>Create Job
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
} else {
|
||||||
|
<form id="edit-job-form" hx-post={ fmt.Sprintf("/jobs/%d", data.Job.ID) } hx-target="body" hx-boost="true" data-config-order={ data.Job.ConfigIDs } class="space-y-6">
|
||||||
|
<!-- Job Details Section -->
|
||||||
|
<div class="p-5 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<h3 class="mb-4 text-xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-tag mr-2 text-blue-500 dark:text-blue-400"></i>Job Details
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Job name field -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
Job Name
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||||
|
<i class="fas fa-tag text-gray-500 dark:text-gray-400"></i>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
id="name"
|
||||||
|
value={ data.Job.Name }
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="Daily Production Backup"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-info-circle mr-1"></i>
|
||||||
|
Descriptive name for this job (optional). If not provided, the config name will be used.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Schedule field -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="schedule" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
Schedule (Cron Expression)
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||||
|
<i class="fas fa-clock text-gray-500 dark:text-gray-400"></i>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="schedule"
|
||||||
|
id="schedule"
|
||||||
|
value={ data.Job.Schedule }
|
||||||
|
required
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="*/15 * * * *"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-info-circle mr-1"></i>
|
||||||
|
Use standard cron expression format. Example: */15 * * * * (every 15 minutes)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Enabled toggle -->
|
||||||
|
<div class="p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="enabled"
|
||||||
|
value="true"
|
||||||
|
id="enabled"
|
||||||
|
if data.Job.GetEnabled() {
|
||||||
|
checked
|
||||||
|
}
|
||||||
|
class="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||||
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">Enable this job</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-info-circle mr-1"></i>
|
||||||
|
Disabled jobs won't run automatically but can still be triggered manually.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Configuration Selection Section -->
|
||||||
|
<div class="p-5 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<h3 class="mb-4 text-xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-cog mr-2 text-blue-500 dark:text-blue-400"></i>Transfer Configurations
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Search field -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="config-search-edit" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
Search Configurations
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||||
|
<i class="fas fa-search text-gray-500 dark:text-gray-400"></i>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="config-search-edit"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="Search configurations..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Configuration list -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
Available Configurations
|
||||||
|
</label>
|
||||||
|
<div class="border border-gray-200 rounded-lg dark:border-gray-700 overflow-hidden">
|
||||||
|
<div class="max-h-56 overflow-y-auto" id="config-list-edit">
|
||||||
|
if len(data.Configs) > 0 {
|
||||||
|
for _, config := range data.Configs {
|
||||||
|
<div class="config-item px-4 py-3 border-b last:border-b-0 border-gray-200 dark:border-gray-700 flex items-center" data-name={ config.Name }>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="config_ids[]"
|
||||||
|
id={ fmt.Sprintf("config_edit_%d", config.ID) }
|
||||||
|
value={ fmt.Sprint(config.ID) }
|
||||||
|
if configSelected(data.Job, config.ID) {
|
||||||
|
checked
|
||||||
|
}
|
||||||
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
<label for={ fmt.Sprintf("config_edit_%d", config.ID) } class="ms-2 text-sm font-medium text-gray-900 dark:text-white cursor-pointer w-full">
|
||||||
|
{ config.Name }
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
<div class="text-center py-4 text-gray-500 dark:text-gray-400">
|
||||||
|
No configurations available. <a href="/configs/new" class="text-blue-600 dark:text-blue-500 hover:underline">Create one</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selected configurations order list -->
|
||||||
|
<div>
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
<i class="fas fa-sort-amount-down mr-1"></i> Execution Order
|
||||||
|
</label>
|
||||||
|
<div id="selected-configs-edit" class="border border-gray-200 dark:border-gray-700 rounded-lg p-4 min-h-16 bg-gray-50 dark:bg-gray-800">
|
||||||
|
<!-- Items will be populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-info-circle mr-1"></i>
|
||||||
|
Use the arrows to change the order in which configurations will execute.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form actions -->
|
||||||
|
<div class="flex items-center justify-between pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<a href="/jobs" class="text-white bg-gray-500 hover:bg-gray-600 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800">
|
||||||
|
<i class="fas fa-arrow-left mr-2"></i>Cancel
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-save mr-2"></i>Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Help Notice -->
|
<!-- Help Card -->
|
||||||
<div class="mt-8 text-center">
|
<div class="mt-4 p-4 bg-gray-50 border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
||||||
<div class="inline-flex items-center text-sm text-secondary-500 dark:text-secondary-400">
|
<div class="flex">
|
||||||
<i class="fas fa-question-circle mr-2 text-primary-500"></i>
|
<div class="flex-shrink-0">
|
||||||
<span>Need help with cron expressions? Try <a href="https://crontab.guru/" target="_blank" class="text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300">crontab.guru</a></span>
|
<i class="fas fa-lightbulb text-yellow-400 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<h5 class="text-sm font-medium text-gray-900 dark:text-white">Scheduling Tips</h5>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Need help with cron expressions? Try <a href="https://crontab.guru/" target="_blank" class="font-medium underline hover:no-underline">crontab.guru</a> for a visual editor. Jobs can run multiple configurations in sequence, useful for multi-step transfer workflows.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+151
-157
@@ -22,169 +22,163 @@ templ JobRunDetails(ctx context.Context, data JobRunDetailsData) {
|
|||||||
// JobRunDetailsContent is the same as JobRunDetails but without the layout wrapper
|
// JobRunDetailsContent is the same as JobRunDetails but without the layout wrapper
|
||||||
// This is used for testing
|
// This is used for testing
|
||||||
templ JobRunDetailsContent(ctx context.Context, data JobRunDetailsData) {
|
templ JobRunDetailsContent(ctx context.Context, data JobRunDetailsData) {
|
||||||
<div class="py-6">
|
<div class="py-6 px-4 mx-auto max-w-7xl lg:px-8">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="mb-6">
|
||||||
<div class="mb-6">
|
<a href="/dashboard" class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 inline-flex items-center">
|
||||||
<a href="/dashboard" class="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300">
|
<i class="fas fa-arrow-left mr-2"></i> Back to Dashboard
|
||||||
<i class="fas fa-arrow-left mr-1"></i> Back to Dashboard
|
</a>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="flex items-center justify-between mb-8">
|
||||||
<div class="flex items-center justify-between mb-8">
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
<h1 class="text-3xl font-bold text-secondary-900 dark:text-secondary-100">
|
<i class="fas fa-file-alt mr-3 text-blue-600 dark:text-blue-400"></i>
|
||||||
<i class="fas fa-file-alt mr-2 text-primary-600 dark:text-primary-400"></i>
|
Job Run Details
|
||||||
Job Run Details
|
</h1>
|
||||||
</h1>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Job Run Information Card -->
|
<!-- Job Run Information Card -->
|
||||||
<div class="bg-white dark:bg-secondary-800 shadow overflow-hidden rounded-lg mb-8">
|
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm mb-8">
|
||||||
<div class="px-4 py-5 sm:px-6 border-b border-secondary-200 dark:border-secondary-700">
|
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<h3 class="text-lg leading-6 font-medium text-secondary-900 dark:text-secondary-100">{ data.Job.Name }</h3>
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">{ data.Job.Name }</h3>
|
||||||
if data.JobHistory.Status == "completed" {
|
if data.JobHistory.Status == "completed" {
|
||||||
<span class="px-3 py-1 inline-flex text-sm leading-5 font-semibold rounded-full bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-300">
|
<span class="px-3 py-1 text-sm font-medium rounded-full bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-300 inline-flex items-center">
|
||||||
<i class="fas fa-check mr-1"></i> Completed
|
<i class="fas fa-check mr-2"></i> Completed
|
||||||
</span>
|
</span>
|
||||||
} else if data.JobHistory.Status == "failed" {
|
} else if data.JobHistory.Status == "failed" {
|
||||||
<span class="px-3 py-1 inline-flex text-sm leading-5 font-semibold rounded-full bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-300">
|
<span class="px-3 py-1 text-sm font-medium rounded-full bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-300 inline-flex items-center">
|
||||||
<i class="fas fa-times mr-1"></i> Failed
|
<i class="fas fa-times mr-2"></i> Failed
|
||||||
</span>
|
</span>
|
||||||
} else {
|
} else {
|
||||||
<span class="px-3 py-1 inline-flex text-sm leading-5 font-semibold rounded-full bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-300">
|
<span class="px-3 py-1 text-sm font-medium rounded-full bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-300 inline-flex items-center">
|
||||||
<i class="fas fa-spinner fa-spin mr-1"></i> Running
|
<i class="fas fa-spinner fa-spin mr-2"></i> Running
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Config: { data.Config.Name }</p>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-5 sm:p-6">
|
||||||
|
<dl class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<div class="sm:col-span-1">
|
||||||
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center mb-1">
|
||||||
|
<i class="fas fa-calendar-alt mr-2 text-gray-400 dark:text-gray-500"></i> Start Time
|
||||||
|
</dt>
|
||||||
|
<dd class="text-sm text-gray-900 dark:text-white">
|
||||||
|
{ data.JobHistory.StartTime.Format("Jan 02, 2006 15:04:05") }
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">Config: { data.Config.Name }</p>
|
<div class="sm:col-span-1">
|
||||||
</div>
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center mb-1">
|
||||||
<div class="px-4 py-5 sm:p-6">
|
<i class="fas fa-calendar-check mr-2 text-gray-400 dark:text-gray-500"></i> End Time
|
||||||
<dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2 lg:grid-cols-3">
|
</dt>
|
||||||
<div class="sm:col-span-1">
|
<dd class="text-sm text-gray-900 dark:text-white">
|
||||||
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
if data.JobHistory.EndTime != nil {
|
||||||
<i class="fas fa-calendar-alt mr-1"></i> Start Time
|
{ data.JobHistory.EndTime.Format("Jan 02, 2006 15:04:05") }
|
||||||
</dt>
|
} else {
|
||||||
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100">
|
<span class="italic text-gray-500 dark:text-gray-400">In progress</span>
|
||||||
{ data.JobHistory.StartTime.Format("Jan 02, 2006 15:04:05") }
|
}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
|
||||||
<div class="sm:col-span-1">
|
|
||||||
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-calendar-check mr-1"></i> End Time
|
|
||||||
</dt>
|
|
||||||
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100">
|
|
||||||
if data.JobHistory.EndTime != nil {
|
|
||||||
{ data.JobHistory.EndTime.Format("Jan 02, 2006 15:04:05") }
|
|
||||||
} else {
|
|
||||||
<span class="italic text-secondary-500">In progress</span>
|
|
||||||
}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div class="sm:col-span-1">
|
|
||||||
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-clock mr-1"></i> Duration
|
|
||||||
</dt>
|
|
||||||
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100">
|
|
||||||
if data.JobHistory.EndTime != nil {
|
|
||||||
{ data.JobHistory.EndTime.Sub(data.JobHistory.StartTime).String() }
|
|
||||||
} else {
|
|
||||||
<span class="italic text-secondary-500">In progress</span>
|
|
||||||
}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div class="sm:col-span-1">
|
|
||||||
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-upload mr-1"></i> Data Transferred
|
|
||||||
</dt>
|
|
||||||
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100">
|
|
||||||
{ formatBytes(data.JobHistory.BytesTransferred) }
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div class="sm:col-span-1">
|
|
||||||
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-file mr-1"></i> Files Transferred
|
|
||||||
</dt>
|
|
||||||
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100">
|
|
||||||
{ fmt.Sprintf("%d files", data.JobHistory.FilesTransferred) }
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div class="sm:col-span-1">
|
|
||||||
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-calendar-day mr-1"></i> Job Schedule
|
|
||||||
</dt>
|
|
||||||
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100">
|
|
||||||
{ data.Job.Schedule }
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Transfer Configuration Details -->
|
|
||||||
<div class="bg-white dark:bg-secondary-800 shadow overflow-hidden rounded-lg mb-8">
|
|
||||||
<div class="px-4 py-5 sm:px-6 border-b border-secondary-200 dark:border-secondary-700">
|
|
||||||
<h3 class="text-lg leading-6 font-medium text-secondary-900 dark:text-secondary-100">
|
|
||||||
<i class="fas fa-cog mr-2 text-primary-600 dark:text-primary-400"></i>
|
|
||||||
Transfer Configuration
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="px-4 py-5 sm:p-6">
|
|
||||||
<dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
|
|
||||||
<div class="sm:col-span-1">
|
|
||||||
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">Source Type</dt>
|
|
||||||
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100">
|
|
||||||
<span class="px-2 py-1 text-xs font-medium rounded bg-secondary-100 dark:bg-secondary-700">{ data.Config.SourceType }</span>
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div class="sm:col-span-1">
|
|
||||||
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">Destination Type</dt>
|
|
||||||
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100">
|
|
||||||
<span class="px-2 py-1 text-xs font-medium rounded bg-secondary-100 dark:bg-secondary-700">{ data.Config.DestinationType }</span>
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div class="sm:col-span-1">
|
|
||||||
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">Source Path</dt>
|
|
||||||
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100 font-mono bg-secondary-50 dark:bg-secondary-900 p-2 rounded">{ data.Config.SourcePath }</dd>
|
|
||||||
</div>
|
|
||||||
<div class="sm:col-span-1">
|
|
||||||
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">Destination Path</dt>
|
|
||||||
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100 font-mono bg-secondary-50 dark:bg-secondary-900 p-2 rounded">{ data.Config.DestinationPath }</dd>
|
|
||||||
</div>
|
|
||||||
<div class="sm:col-span-1">
|
|
||||||
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">File Pattern</dt>
|
|
||||||
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100 font-mono">{ data.Config.FilePattern }</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Error Information (if any) -->
|
|
||||||
if data.JobHistory.Status == "failed" && data.JobHistory.ErrorMessage != "" {
|
|
||||||
<div class="bg-white dark:bg-secondary-800 shadow overflow-hidden rounded-lg mb-8 border-l-4 border-red-500">
|
|
||||||
<div class="px-4 py-5 sm:px-6 border-b border-secondary-200 dark:border-secondary-700">
|
|
||||||
<h3 class="text-lg leading-6 font-medium text-red-600 dark:text-red-400">
|
|
||||||
<i class="fas fa-exclamation-triangle mr-2"></i>
|
|
||||||
Error Information
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="px-4 py-5 sm:p-6">
|
<div class="sm:col-span-1">
|
||||||
<div class="bg-red-50 dark:bg-red-900/20 p-4 rounded-lg">
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center mb-1">
|
||||||
<pre class="text-sm text-red-800 dark:text-red-300 whitespace-pre-wrap font-mono">{ data.JobHistory.ErrorMessage }</pre>
|
<i class="fas fa-clock mr-2 text-gray-400 dark:text-gray-500"></i> Duration
|
||||||
</div>
|
</dt>
|
||||||
|
<dd class="text-sm text-gray-900 dark:text-white">
|
||||||
|
if data.JobHistory.EndTime != nil {
|
||||||
|
{ data.JobHistory.EndTime.Sub(data.JobHistory.StartTime).String() }
|
||||||
|
} else {
|
||||||
|
<span class="italic text-gray-500 dark:text-gray-400">In progress</span>
|
||||||
|
}
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="sm:col-span-1">
|
||||||
}
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center mb-1">
|
||||||
|
<i class="fas fa-upload mr-2 text-gray-400 dark:text-gray-500"></i> Data Transferred
|
||||||
<!-- Action Buttons -->
|
</dt>
|
||||||
<div class="flex flex-col sm:flex-row gap-4 mt-8">
|
<dd class="text-sm text-gray-900 dark:text-white">
|
||||||
<a href="/jobs" class="btn-secondary text-center flex items-center justify-center">
|
{ formatBytes(data.JobHistory.BytesTransferred) }
|
||||||
<i class="fas fa-list-ul mr-2"></i> View All Jobs
|
</dd>
|
||||||
</a>
|
</div>
|
||||||
<a href={ templ.SafeURL(fmt.Sprintf("/jobs/%d", data.Job.ID)) } class="btn-primary text-center flex items-center justify-center">
|
<div class="sm:col-span-1">
|
||||||
<i class="fas fa-edit mr-2"></i> Edit Job
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center mb-1">
|
||||||
</a>
|
<i class="fas fa-file mr-2 text-gray-400 dark:text-gray-500"></i> Files Transferred
|
||||||
|
</dt>
|
||||||
|
<dd class="text-sm text-gray-900 dark:text-white">
|
||||||
|
{ fmt.Sprintf("%d files", data.JobHistory.FilesTransferred) }
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-1">
|
||||||
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center mb-1">
|
||||||
|
<i class="fas fa-calendar-day mr-2 text-gray-400 dark:text-gray-500"></i> Job Schedule
|
||||||
|
</dt>
|
||||||
|
<dd class="text-sm text-gray-900 dark:text-white">
|
||||||
|
{ data.Job.Schedule }
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Transfer Configuration Details -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm mb-8">
|
||||||
|
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-cog mr-3 text-blue-600 dark:text-blue-400"></i>
|
||||||
|
Transfer Configuration
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-5 sm:p-6">
|
||||||
|
<dl class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-2">
|
||||||
|
<div class="sm:col-span-1">
|
||||||
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Source Type</dt>
|
||||||
|
<dd class="text-sm text-gray-900 dark:text-white">
|
||||||
|
<span class="px-2.5 py-0.5 text-xs font-medium rounded bg-gray-100 dark:bg-gray-700">{ data.Config.SourceType }</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-1">
|
||||||
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Destination Type</dt>
|
||||||
|
<dd class="text-sm text-gray-900 dark:text-white">
|
||||||
|
<span class="px-2.5 py-0.5 text-xs font-medium rounded bg-gray-100 dark:bg-gray-700">{ data.Config.DestinationType }</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-1">
|
||||||
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Source Path</dt>
|
||||||
|
<dd class="text-sm text-gray-900 dark:text-white font-mono bg-gray-50 dark:bg-gray-900 p-2 rounded-lg border border-gray-200 dark:border-gray-700">{ data.Config.SourcePath }</dd>
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-1">
|
||||||
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Destination Path</dt>
|
||||||
|
<dd class="text-sm text-gray-900 dark:text-white font-mono bg-gray-50 dark:bg-gray-900 p-2 rounded-lg border border-gray-200 dark:border-gray-700">{ data.Config.DestinationPath }</dd>
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-1">
|
||||||
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">File Pattern</dt>
|
||||||
|
<dd class="text-sm text-gray-900 dark:text-white font-mono">{ data.Config.FilePattern }</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Information (if any) -->
|
||||||
|
if data.JobHistory.Status == "failed" && data.JobHistory.ErrorMessage != "" {
|
||||||
|
<div class="p-4 mb-8 text-red-800 border-l-4 border-red-300 bg-red-50 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800 rounded-lg">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<i class="fas fa-exclamation-triangle flex-shrink-0 mr-2 text-red-600 dark:text-red-500"></i>
|
||||||
|
<h3 class="text-lg font-medium">Error Information</h3>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<pre class="text-sm whitespace-pre-wrap font-mono p-3 bg-white dark:bg-gray-900 rounded-lg border border-red-200 dark:border-red-800">{ data.JobHistory.ErrorMessage }</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 mt-8">
|
||||||
|
<a href="/jobs" class="text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700 inline-flex items-center justify-center">
|
||||||
|
<i class="fas fa-list-ul mr-2"></i> View All Jobs
|
||||||
|
</a>
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/jobs/%d", data.Job.ID)) } class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800 inline-flex items-center justify-center">
|
||||||
|
<i class="fas fa-edit mr-2"></i> Edit Job
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+275
-189
@@ -6,55 +6,46 @@ import (
|
|||||||
"github.com/starfleetcptn/gomft/internal/db"
|
"github.com/starfleetcptn/gomft/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Dialog component for confirmation dialogs - copied from admin_tools.templ
|
// Dialog component for confirmation dialogs using Flowbite modal
|
||||||
templ JobDialog(id string, title string, message string, confirmClass string, confirmText string, action string, jobID uint, jobName string) {
|
templ JobDialog(id string, title string, message string, confirmClass string, confirmText string, action string, jobID uint, jobName string) {
|
||||||
<div id={ id } class="hidden fixed inset-0 bg-secondary-900/50 dark:bg-secondary-900/80 backdrop-blur-sm z-50 flex items-center justify-center">
|
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
||||||
<div class="bg-white dark:bg-secondary-800 rounded-lg shadow-xl max-w-md w-full mx-4 overflow-hidden">
|
<!-- Backdrop -->
|
||||||
<div class="px-6 pt-5 pb-3 text-center">
|
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
|
||||||
<div class="flex justify-center mb-2">
|
<!-- Modal content -->
|
||||||
<i class="fas fa-exclamation-triangle text-yellow-400 text-3xl"></i>
|
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
|
||||||
|
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||||
|
<div class="p-6 text-center">
|
||||||
|
if action == "delete" {
|
||||||
|
<i class="fas fa-trash-alt text-red-400 text-3xl mb-4"></i>
|
||||||
|
} else {
|
||||||
|
<i class="fas fa-exclamation-triangle text-yellow-400 text-3xl mb-4"></i>
|
||||||
|
}
|
||||||
|
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{ message }</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={ confirmClass }
|
||||||
|
hx-delete={ fmt.Sprintf("/jobs/%d", jobID) }
|
||||||
|
hx-target="closest li"
|
||||||
|
hx-swap="delete"
|
||||||
|
data-job-name={ jobName }
|
||||||
|
data-job-id={ fmt.Sprint(jobID) }
|
||||||
|
id={ fmt.Sprintf("delete-btn-%d", jobID) }
|
||||||
|
onclick={ triggerJobDelete(id, jobID, jobName) }>
|
||||||
|
{ confirmText }
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick={ closeModal(id) } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-medium text-secondary-900 dark:text-secondary-100">
|
|
||||||
{ title }
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="px-6 py-4 text-center">
|
|
||||||
<p class="text-secondary-700 dark:text-secondary-300">
|
|
||||||
{ message }
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="px-6 py-4 flex justify-end space-x-3">
|
|
||||||
<button type="button" class="btn-secondary" onclick={ hideJobDialog(id) }>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={ confirmClass }
|
|
||||||
hx-delete={ fmt.Sprintf("/jobs/%d", jobID) }
|
|
||||||
hx-target="closest li"
|
|
||||||
hx-swap="delete"
|
|
||||||
data-job-name={ jobName }
|
|
||||||
data-job-id={ fmt.Sprint(jobID) }
|
|
||||||
id={ fmt.Sprintf("delete-btn-%d", jobID) }
|
|
||||||
onclick={ triggerJobDelete(id, jobID, jobName) }>
|
|
||||||
{ confirmText }
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
script hideJobDialog(id string) {
|
|
||||||
document.getElementById(id).classList.add("hidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
script showJobDialog(id string) {
|
|
||||||
document.getElementById(id).classList.remove("hidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
script triggerJobDelete(dialogId string, jobID uint, jobName string) {
|
script triggerJobDelete(dialogId string, jobID uint, jobName string) {
|
||||||
// Hide the dialog
|
// Hide the dialog
|
||||||
document.getElementById(dialogId).classList.add("hidden");
|
document.getElementById(dialogId).classList.add("hidden");
|
||||||
|
document.getElementById(dialogId).classList.remove("flex");
|
||||||
|
|
||||||
// Add debugging info
|
// Add debugging info
|
||||||
console.log(`Job deletion triggered for: ${jobName} (ID: ${jobID})`);
|
console.log(`Job deletion triggered for: ${jobName} (ID: ${jobID})`);
|
||||||
@@ -75,38 +66,84 @@ type JobsData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
templ Jobs(ctx context.Context, data JobsData) {
|
templ Jobs(ctx context.Context, data JobsData) {
|
||||||
@LayoutWithContext("Transfer Jobs", ctx) {
|
@LayoutWithContext("Scheduled Jobs", ctx) {
|
||||||
|
<!-- Status and Error Messages -->
|
||||||
|
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Debug notification system
|
// Debug notification system
|
||||||
console.log("Jobs template loaded, setting up notification system");
|
console.log("Jobs template loaded, setting up notification system");
|
||||||
|
|
||||||
// Create a global notyf instance immediately
|
// Function to create and show a toast
|
||||||
window.notyf = new Notyf({
|
function showToast(message, type) {
|
||||||
duration: 3000,
|
const toastContainer = document.getElementById('toast-container');
|
||||||
position: {
|
|
||||||
x: 'right',
|
// Create toast element
|
||||||
y: 'top',
|
const toast = document.createElement('div');
|
||||||
},
|
toast.id = 'toast-' + type + '-' + Date.now();
|
||||||
types: [
|
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
||||||
{
|
toast.role = 'alert';
|
||||||
type: 'success',
|
|
||||||
background: '#38c172',
|
// Set toast content based on type
|
||||||
icon: {
|
let iconClass, bgColorClass, textColorClass;
|
||||||
className: 'fas fa-check-circle',
|
|
||||||
tagName: 'i'
|
if (type === 'success') {
|
||||||
}
|
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
||||||
},
|
bgColorClass = 'text-green-500 dark:text-green-200';
|
||||||
{
|
textColorClass = 'text-green-500 dark:text-green-200';
|
||||||
type: 'error',
|
} else if (type === 'error') {
|
||||||
background: '#e3342f',
|
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
||||||
icon: {
|
bgColorClass = 'text-red-500 dark:text-red-200';
|
||||||
className: 'fas fa-exclamation-circle',
|
textColorClass = 'text-red-500 dark:text-red-200';
|
||||||
tagName: 'i'
|
} else {
|
||||||
}
|
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
||||||
}
|
bgColorClass = 'text-blue-500 dark:text-blue-200';
|
||||||
]
|
textColorClass = 'text-blue-500 dark:text-blue-200';
|
||||||
});
|
}
|
||||||
console.log("Notyf initialized:", window.notyf);
|
|
||||||
|
// Set inner HTML with appropriate icon and message
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
||||||
|
${type === 'success'
|
||||||
|
? '<i class="fas fa-check"></i>'
|
||||||
|
: type === 'error'
|
||||||
|
? '<i class="fas fa-exclamation-circle"></i>'
|
||||||
|
: '<i class="fas fa-info-circle"></i>'}
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 text-sm font-normal">${message}</div>
|
||||||
|
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add toast to container
|
||||||
|
toastContainer.appendChild(toast);
|
||||||
|
|
||||||
|
// Trigger animation after a small delay to ensure the DOM has updated
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('translate-y-16', 'opacity-0');
|
||||||
|
toast.classList.add('translate-y-0', 'opacity-100');
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// Add event listener to close button
|
||||||
|
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
||||||
|
closeButton.addEventListener('click', function() {
|
||||||
|
// Animate out before removing
|
||||||
|
toast.classList.add('opacity-0', 'translate-y-4');
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-remove toast after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.add('opacity-0', 'translate-y-4');
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
// Global function to handle job running
|
// Global function to handle job running
|
||||||
window.runJob = function(button) {
|
window.runJob = function(button) {
|
||||||
@@ -126,22 +163,33 @@ templ Jobs(ctx context.Context, data JobsData) {
|
|||||||
displayName = event.detail.headers['HX-Job-Name'];
|
displayName = event.detail.headers['HX-Job-Name'];
|
||||||
}
|
}
|
||||||
console.log(`Showing success notification for job: ${displayName}`);
|
console.log(`Showing success notification for job: ${displayName}`);
|
||||||
window.notyf.success(`Job "${displayName}" started successfully!`);
|
showToast(`Job "${displayName}" started successfully!`, 'success');
|
||||||
} else {
|
} else {
|
||||||
let errorMsg = `Failed to start job "${jobName}"`;
|
let errorMsg = `Failed to start job "${jobName}"`;
|
||||||
if (event.detail.xhr && event.detail.xhr.responseText) {
|
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||||
errorMsg = `Error: ${event.detail.xhr.responseText}`;
|
errorMsg = `Error: ${event.detail.xhr.responseText}`;
|
||||||
}
|
}
|
||||||
console.log(`Showing error notification: ${errorMsg}`);
|
console.log(`Showing error notification: ${errorMsg}`);
|
||||||
window.notyf.error(errorMsg);
|
showToast(errorMsg, 'error');
|
||||||
}
|
}
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle modal hide buttons
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const hideButtons = document.querySelectorAll('[data-modal-hide]');
|
||||||
|
hideButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const modalId = this.getAttribute('data-modal-hide');
|
||||||
|
const modal = document.getElementById(modalId);
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Track all HTMX events for debugging
|
// Track all HTMX events for debugging
|
||||||
document.addEventListener('htmx:beforeRequest', function(event) {
|
document.addEventListener('htmx:beforeRequest', function(event) {
|
||||||
console.log("HTMX before request:", event.detail);
|
|
||||||
|
|
||||||
// Check if this is a DELETE request by examining the URL and method
|
// Check if this is a DELETE request by examining the URL and method
|
||||||
const path = event.detail.path;
|
const path = event.detail.path;
|
||||||
const method = event.detail.verb;
|
const method = event.detail.verb;
|
||||||
@@ -158,9 +206,7 @@ templ Jobs(ctx context.Context, data JobsData) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Track HTMX after-request events for job deletion
|
// Track HTMX after-request events for job deletion
|
||||||
document.addEventListener('htmx:afterRequest', function(event) {
|
document.addEventListener('htmx:afterRequest', function(event) {
|
||||||
console.log("HTMX after request:", event.detail);
|
|
||||||
|
|
||||||
// Check for job deletion multiple ways
|
// Check for job deletion multiple ways
|
||||||
const isDeleteRequest =
|
const isDeleteRequest =
|
||||||
// Check global flag from the triggerJobDelete function
|
// Check global flag from the triggerJobDelete function
|
||||||
@@ -191,7 +237,7 @@ templ Jobs(ctx context.Context, data JobsData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Showing success notification for deleted job: ${jobName}`);
|
console.log(`Showing success notification for deleted job: ${jobName}`);
|
||||||
window.notyf.success(`Job "${jobName}" deleted successfully`);
|
showToast(`Job "${jobName}" deleted successfully`, 'success');
|
||||||
|
|
||||||
// Clear flags
|
// Clear flags
|
||||||
window.currentlyDeletingJob = false;
|
window.currentlyDeletingJob = false;
|
||||||
@@ -234,7 +280,7 @@ templ Jobs(ctx context.Context, data JobsData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Showing error notification: ${errorMsg}`);
|
console.log(`Showing error notification: ${errorMsg}`);
|
||||||
window.notyf.error(errorMsg);
|
showToast(errorMsg, 'error');
|
||||||
|
|
||||||
// Clear flags
|
// Clear flags
|
||||||
window.currentlyDeletingJob = false;
|
window.currentlyDeletingJob = false;
|
||||||
@@ -244,147 +290,187 @@ templ Jobs(ctx context.Context, data JobsData) {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="py-6">
|
<div id="jobs-container" style="min-height: 100vh; background-color: rgb(249, 250, 251);" class="jobs-page bg-gray-50 dark:bg-gray-900">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="pb-8 w-full">
|
||||||
<div class="flex justify-between items-center mb-8">
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
<h1 class="text-3xl font-bold text-secondary-900 dark:text-secondary-100">
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
<i class="fas fa-exchange-alt mr-2 text-primary-600 dark:text-primary-400"></i>
|
<i class="fas fa-calendar-alt w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||||
Transfer Jobs
|
Scheduled Jobs
|
||||||
</h1>
|
</h1>
|
||||||
<a href="/jobs/new" class="btn-primary">
|
<a href="/jobs/new" class="flex items-center justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
<i class="fas fa-plus mr-2"></i>
|
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||||
New Job
|
New Job
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
if len(data.Jobs) == 0 {
|
if len(data.Jobs) == 0 {
|
||||||
<div class="card p-12 flex flex-col items-center justify-center text-center">
|
<div class="bg-white dark:bg-gray-800 shadow-md rounded-lg p-12 flex flex-col items-center justify-center text-center">
|
||||||
<div class="inline-block p-4 rounded-full bg-secondary-100 dark:bg-secondary-700 mb-4">
|
<div class="inline-block p-4 rounded-full bg-gray-100 dark:bg-gray-700 mb-4">
|
||||||
<i class="fas fa-tasks text-secondary-400 dark:text-secondary-500 text-3xl"></i>
|
<i class="fas fa-list text-3xl text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="mt-2 text-lg font-medium text-secondary-900 dark:text-secondary-100">No jobs</h3>
|
<h3 class="mt-2 text-lg font-medium text-gray-900 dark:text-white">No jobs</h3>
|
||||||
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">Get started by creating a new transfer job.</p>
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Get started by creating a new transfer job.</p>
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<a href="/jobs/new" class="btn-primary">
|
<a href="/jobs/new" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
<i class="fas fa-plus mr-2"></i>
|
<i class="fas fa-plus mr-2"></i>
|
||||||
New Job
|
New Job
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
<div class="card overflow-hidden">
|
<div class="bg-white dark:bg-gray-800 shadow-md sm:rounded-lg overflow-hidden">
|
||||||
<ul role="list" class="divide-y divide-secondary-200 dark:divide-secondary-700">
|
<div class="relative overflow-x-auto">
|
||||||
for _, job := range data.Jobs {
|
<ul role="list" class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<li>
|
for _, job := range data.Jobs {
|
||||||
<div class="block hover:bg-secondary-50 dark:hover:bg-secondary-750 transition-colors">
|
<li>
|
||||||
<div class="px-4 py-4 sm:px-6">
|
<div class="block hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||||
<div class="flex items-center justify-between">
|
<div class="px-4 py-4 sm:px-6">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center justify-between">
|
||||||
<p class="text-sm font-medium text-primary-600 dark:text-primary-400 truncate">
|
<div class="flex items-center">
|
||||||
if job.Name != "" {
|
<p class="text-sm font-medium text-blue-600 dark:text-blue-400 truncate">
|
||||||
{ job.Name }
|
if job.Name != "" {
|
||||||
} else {
|
{ job.Name }
|
||||||
{ job.Config.Name }
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
if job.GetEnabled() {
|
|
||||||
<span class="ml-2 px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-300">
|
|
||||||
Active
|
|
||||||
</span>
|
|
||||||
} else {
|
|
||||||
<span class="ml-2 px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-secondary-100 dark:bg-secondary-700 text-secondary-800 dark:text-secondary-300">
|
|
||||||
Inactive
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="ml-2 flex-shrink-0 flex space-x-2">
|
|
||||||
<button
|
|
||||||
hx-post={ fmt.Sprintf("/jobs/%d/run", job.ID) }
|
|
||||||
hx-swap="none"
|
|
||||||
class="btn-primary btn-sm"
|
|
||||||
data-job-id={ fmt.Sprint(job.ID) }
|
|
||||||
data-job-name={ job.Name }
|
|
||||||
onclick="window.runJob(this)">
|
|
||||||
<i class="fas fa-play mr-1"></i>
|
|
||||||
Run Now
|
|
||||||
</button>
|
|
||||||
<a href={ templ.SafeURL(fmt.Sprintf("/jobs/%d", job.ID)) } class="btn-secondary btn-sm">
|
|
||||||
<i class="fas fa-edit mr-1"></i>
|
|
||||||
Edit
|
|
||||||
</a>
|
|
||||||
<!-- Add delete dialog for each job -->
|
|
||||||
@JobDialog(
|
|
||||||
fmt.Sprintf("delete-job-dialog-%d", job.ID),
|
|
||||||
"Delete Job",
|
|
||||||
fmt.Sprintf("Are you sure you want to delete the job '%s'? This cannot be undone.", determineJobName(job)),
|
|
||||||
"btn-danger",
|
|
||||||
"Delete",
|
|
||||||
"delete",
|
|
||||||
job.ID,
|
|
||||||
determineJobName(job),
|
|
||||||
)
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick={ showJobDialog(fmt.Sprintf("delete-job-dialog-%d", job.ID)) }
|
|
||||||
class="btn-danger btn-sm">
|
|
||||||
<i class="fas fa-trash-alt mr-1"></i>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 sm:flex sm:justify-between">
|
|
||||||
<div class="sm:flex">
|
|
||||||
<p class="flex items-center text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-cogs flex-shrink-0 mr-1.5 h-5 w-5 text-secondary-400 dark:text-secondary-500"></i>
|
|
||||||
Configs:
|
|
||||||
<span class="ml-1">
|
|
||||||
if count, ok := data.ConfigCount[job.ID]; ok && count > 1 {
|
|
||||||
{ fmt.Sprintf("%d configurations", count) }
|
|
||||||
} else if job.ConfigID > 0 {
|
|
||||||
{ job.Config.Name }
|
|
||||||
} else {
|
} else {
|
||||||
{ "None" }
|
{ job.Config.Name }
|
||||||
}
|
}
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p class="mt-2 flex items-center text-sm text-secondary-500 dark:text-secondary-400 sm:mt-0 sm:ml-6">
|
|
||||||
<i class="fas fa-calendar-alt flex-shrink-0 mr-1.5 h-5 w-5 text-secondary-400 dark:text-secondary-500"></i>
|
|
||||||
Schedule: { job.Schedule }
|
|
||||||
</p>
|
|
||||||
if job.LastRun != nil {
|
|
||||||
<p class="mt-2 flex items-center text-sm text-secondary-500 dark:text-secondary-400 sm:mt-0 sm:ml-6">
|
|
||||||
<i class="fas fa-history flex-shrink-0 mr-1.5 h-5 w-5 text-secondary-400 dark:text-secondary-500"></i>
|
|
||||||
Last Run: { job.LastRun.Format("2006-01-02 15:04:05") }
|
|
||||||
</p>
|
</p>
|
||||||
|
if job.GetEnabled() {
|
||||||
|
<span class="inline-flex items-center bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300 ml-2">
|
||||||
|
<i class="fas fa-circle-check text-green-500 dark:text-green-400 mr-1"></i>
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
} else {
|
||||||
|
<span class="inline-flex items-center bg-gray-100 text-gray-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded-full dark:bg-gray-700 dark:text-gray-300 ml-2">
|
||||||
|
<i class="fas fa-circle-xmark text-gray-500 dark:text-gray-400 mr-1"></i>
|
||||||
|
Inactive
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 flex-shrink-0 flex space-x-2">
|
||||||
|
<button
|
||||||
|
hx-post={ fmt.Sprintf("/jobs/%d/run", job.ID) }
|
||||||
|
hx-swap="none"
|
||||||
|
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-xs px-3 py-1.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||||
|
data-job-id={ fmt.Sprint(job.ID) }
|
||||||
|
data-job-name={ job.Name }
|
||||||
|
onclick="window.runJob(this)">
|
||||||
|
<i class="fas fa-play mr-1"></i>
|
||||||
|
Run Now
|
||||||
|
</button>
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/jobs/%d", job.ID)) } class="py-1.5 px-3 text-xs font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
|
||||||
|
<i class="fas fa-pen-to-square mr-1"></i>
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
hx-post={ fmt.Sprintf("/jobs/%d/duplicate", job.ID) }
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-target="body"
|
||||||
|
class="text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:ring-4 focus:outline-none focus:ring-indigo-300 font-medium rounded-lg text-xs px-3 py-1.5 text-center inline-flex items-center dark:bg-indigo-700 dark:text-indigo-300 dark:hover:bg-indigo-600 dark:focus:ring-indigo-800"
|
||||||
|
data-job-id={ fmt.Sprint(job.ID) }
|
||||||
|
data-job-name={ job.Name }>
|
||||||
|
<i class="fas fa-clone mr-1"></i>
|
||||||
|
Duplicate
|
||||||
|
</button>
|
||||||
|
<!-- Add delete dialog for each job -->
|
||||||
|
@JobDialog(
|
||||||
|
fmt.Sprintf("delete-job-dialog-%d", job.ID),
|
||||||
|
"Delete Job",
|
||||||
|
fmt.Sprintf("Are you sure you want to delete the job '%s'? This cannot be undone.", determineJobName(job)),
|
||||||
|
"text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800",
|
||||||
|
"Delete",
|
||||||
|
"delete",
|
||||||
|
job.ID,
|
||||||
|
determineJobName(job),
|
||||||
|
)
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={ showModal(fmt.Sprintf("delete-job-dialog-%d", job.ID)) }
|
||||||
|
class="focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-xs px-3 py-1.5 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900">
|
||||||
|
<i class="fas fa-trash mr-1"></i>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 sm:flex sm:justify-between">
|
||||||
|
<div class="sm:flex flex-wrap">
|
||||||
|
<p class="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-gear text-gray-400 dark:text-gray-500 mr-1.5"></i>
|
||||||
|
Configs:
|
||||||
|
<span class="ml-1">
|
||||||
|
if count, ok := data.ConfigCount[job.ID]; ok && count > 1 {
|
||||||
|
{ fmt.Sprintf("%d configurations", count) }
|
||||||
|
} else if job.ConfigID > 0 {
|
||||||
|
{ job.Config.Name }
|
||||||
|
} else {
|
||||||
|
{ "None" }
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 flex items-center text-sm text-gray-500 dark:text-gray-400 sm:mt-0 sm:ml-6">
|
||||||
|
<i class="fas fa-calendar text-gray-400 dark:text-gray-500 mr-1.5"></i>
|
||||||
|
Schedule: { job.Schedule }
|
||||||
|
</p>
|
||||||
|
if job.LastRun != nil {
|
||||||
|
<p class="mt-2 flex items-center text-sm text-gray-500 dark:text-gray-400 sm:mt-0 sm:ml-6">
|
||||||
|
<i class="fas fa-clock text-gray-400 dark:text-gray-500 mr-1.5"></i>
|
||||||
|
Last Run: { job.LastRun.Format("2006-01-02 15:04:05") }
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
if job.NextRun != nil {
|
||||||
|
<div class="mt-2 flex items-center text-sm text-gray-500 dark:text-gray-400 sm:mt-0">
|
||||||
|
<i class="fas fa-hourglass-start text-gray-400 dark:text-gray-500 mr-1.5"></i>
|
||||||
|
<p>
|
||||||
|
Next Run: { job.NextRun.Format("2006-01-02 15:04:05") }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
if job.NextRun != nil {
|
|
||||||
<div class="mt-2 flex items-center text-sm text-secondary-500 dark:text-secondary-400 sm:mt-0">
|
|
||||||
<i class="fas fa-clock flex-shrink-0 mr-1.5 h-5 w-5 text-secondary-400 dark:text-secondary-500"></i>
|
|
||||||
<p>
|
|
||||||
Next Run: { job.NextRun.Format("2006-01-02 15:04:05") }
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
}
|
||||||
}
|
</ul>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Help Notice -->
|
<!-- Help Notice -->
|
||||||
<div class="mt-8 text-center">
|
<div class="mt-8 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700">
|
||||||
<p class="text-sm text-secondary-500 dark:text-secondary-400">
|
<div class="flex">
|
||||||
<i class="fas fa-info-circle mr-1 text-primary-500"></i>
|
<div class="flex-shrink-0">
|
||||||
Transfer jobs run according to their schedule and transfer files between configured sources and destinations
|
<i class="fas fa-info-circle text-blue-400 dark:text-blue-400"></i>
|
||||||
</p>
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<p class="text-sm text-blue-700 dark:text-blue-400">
|
||||||
|
Transfer jobs run according to their schedule and transfer files between configured sources and destinations
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Set dark background color if in dark mode
|
||||||
|
if (document.documentElement.classList.contains('dark')) {
|
||||||
|
document.getElementById('jobs-container').style.backgroundColor = '#111827';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener for theme changes
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener('click', function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
document.getElementById('jobs-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+414
-408
@@ -2,15 +2,17 @@ package components
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"time"
|
"time"
|
||||||
"fmt"
|
"github.com/starfleetcptn/gomft/components/shared/toast"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AppVersion will be set at build time using ldflags
|
// AppVersion will be set at build time using ldflags
|
||||||
// Example build command:
|
// Example build command:
|
||||||
// go build -ldflags "-X github.com/starfleetcptn/gomft/components.AppVersion=1.2.3"
|
// go build -ldflags "-X github.com/starfleetcptn/gomft/components.AppVersion=1.2.3"
|
||||||
var AppVersion = "dev"
|
var AppVersion = "DEV"
|
||||||
|
|
||||||
// GetReleaseURL returns the URL to the specific GitHub release
|
// GetReleaseURL returns the URL to the specific GitHub release
|
||||||
func GetReleaseURL() templ.SafeURL {
|
func GetReleaseURL() templ.SafeURL {
|
||||||
@@ -41,465 +43,469 @@ templ Layout(title string) {
|
|||||||
|
|
||||||
templ LayoutWithContext(title string, ctx context.Context) {
|
templ LayoutWithContext(title string, ctx context.Context) {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" class="light h-full p-0 m-0">
|
<html lang="en" class="h-full bg-gray-50 dark:bg-gray-900">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
|
||||||
<meta name="theme-color" content="#2563eb"/>
|
<meta name="theme-color" content="#2563eb"/>
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes"/>
|
<meta name="apple-mobile-web-app-capable" content="yes"/>
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png"/>
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png"/>
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png"/>
|
||||||
<link rel="manifest" href="/static/site.webmanifest">
|
<link rel="manifest" href="/static/site.webmanifest"/>
|
||||||
<title>{ title } - GoMFT</title>
|
<title>{ title } - GoMFT</title>
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
<!-- Font Awesome -->
|
||||||
<script src="https://unpkg.com/alpinejs@3.13.5/dist/cdn.min.js" defer></script>
|
<link rel="stylesheet" href="/static/dist/fontawesome/css/all.min.css"/>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<!-- Application assets -->
|
||||||
<script src="/static/js/app.js"></script>
|
<link rel="stylesheet" href="/static/dist/app.css"/>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"/>
|
||||||
<link rel="stylesheet" href="/static/css/app.css"/>
|
|
||||||
<!-- Notyf Toast Notifications -->
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/notyf@3/notyf.min.css" />
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/notyf@3/notyf.min.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
// Initialize Notyf and make it available globally
|
// Immediate theme application
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
// This script runs before the DOM is fully loaded
|
||||||
window.notyfInstance = new Notyf({
|
(function() {
|
||||||
duration: 3000,
|
const isDark = localStorage.theme === 'dark' ||
|
||||||
position: {
|
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
x: 'right',
|
|
||||||
y: 'bottom',
|
|
||||||
},
|
|
||||||
types: [
|
|
||||||
{
|
|
||||||
type: 'success',
|
|
||||||
className: 'notyf__toast--success',
|
|
||||||
background: '#10B981',
|
|
||||||
icon: {
|
|
||||||
className: 'fas fa-check-circle',
|
|
||||||
tagName: 'i'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'error',
|
|
||||||
className: 'notyf__toast--error',
|
|
||||||
background: '#EF4444',
|
|
||||||
icon: {
|
|
||||||
className: 'fas fa-exclamation-circle',
|
|
||||||
tagName: 'i'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
if (isDark) {
|
||||||
</script>
|
document.documentElement.classList.add('dark');
|
||||||
<script>
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
tailwind.config = {
|
document.body.classList.add('dark');
|
||||||
darkMode: 'class',
|
|
||||||
theme: {
|
// Apply dark theme to containers
|
||||||
extend: {
|
const containers = ['jobs-container', 'configs-container'];
|
||||||
colors: {
|
containers.forEach(function(id) {
|
||||||
primary: {
|
const container = document.getElementById(id);
|
||||||
50: '#f0f9ff',
|
if (container) {
|
||||||
100: '#e0f2fe',
|
container.classList.add('dark');
|
||||||
200: '#bae6fd',
|
container.style.backgroundColor = '#111827';
|
||||||
300: '#7dd3fc',
|
}
|
||||||
400: '#38bdf8',
|
});
|
||||||
500: '#0ea5e9',
|
});
|
||||||
600: '#0284c7',
|
|
||||||
700: '#0369a1',
|
|
||||||
800: '#075985',
|
|
||||||
900: '#0c4a6e',
|
|
||||||
950: '#082f49',
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
50: '#f8fafc',
|
|
||||||
100: '#f1f5f9',
|
|
||||||
200: '#e2e8f0',
|
|
||||||
300: '#cbd5e1',
|
|
||||||
400: '#94a3b8',
|
|
||||||
500: '#64748b',
|
|
||||||
600: '#475569',
|
|
||||||
700: '#334155',
|
|
||||||
800: '#1e293b',
|
|
||||||
900: '#0f172a',
|
|
||||||
950: '#020617',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fontFamily: {
|
|
||||||
sans: ['Inter var', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'sans-serif'],
|
|
||||||
},
|
|
||||||
boxShadow: {
|
|
||||||
'custom': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
|
|
||||||
'custom-lg': '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style type="text/tailwindcss">
|
<style>
|
||||||
/* Reset default browser margins and padding */
|
|
||||||
html, body {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow-x: hidden; /* Prevent horizontal scrolling */
|
overflow-x: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
|
||||||
.btn-primary {
|
|
||||||
@apply px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-all duration-200 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2;
|
|
||||||
}
|
|
||||||
.btn-secondary {
|
|
||||||
@apply px-4 py-2 bg-secondary-200 text-secondary-800 rounded-lg hover:bg-secondary-300 transition-all duration-200 shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-secondary-300 focus:ring-offset-2 dark:bg-secondary-700 dark:text-secondary-100 dark:hover:bg-secondary-600;
|
|
||||||
}
|
|
||||||
.btn-danger {
|
|
||||||
@apply px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-all duration-200 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2;
|
|
||||||
}
|
|
||||||
.btn-warning {
|
|
||||||
@apply px-4 py-2 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 transition-all duration-200 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:ring-offset-2;
|
|
||||||
}
|
|
||||||
.form-input {
|
|
||||||
@apply w-full px-3 py-2 border border-secondary-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 shadow-sm transition-all duration-200 dark:bg-secondary-800 dark:border-secondary-700 dark:text-white;
|
|
||||||
}
|
|
||||||
.form-checkbox {
|
|
||||||
@apply rounded border-secondary-300 text-primary-600 shadow-sm focus:border-primary-300 focus:ring focus:ring-primary-200 focus:ring-opacity-50 dark:border-secondary-700 dark:bg-secondary-800;
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
@apply bg-white dark:bg-secondary-800 rounded-xl shadow-custom overflow-hidden border border-secondary-200 dark:border-secondary-700 transition-all duration-200 hover:shadow-custom-lg;
|
|
||||||
}
|
|
||||||
.card-header {
|
|
||||||
@apply px-6 py-4 border-b border-secondary-200 dark:border-secondary-700 bg-secondary-50 dark:bg-secondary-900;
|
|
||||||
}
|
|
||||||
.card-body {
|
|
||||||
@apply p-6;
|
|
||||||
}
|
|
||||||
.badge {
|
|
||||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
|
|
||||||
}
|
|
||||||
.badge-success {
|
|
||||||
@apply bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100;
|
|
||||||
}
|
|
||||||
.badge-warning {
|
|
||||||
@apply bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100;
|
|
||||||
}
|
|
||||||
.badge-danger {
|
|
||||||
@apply bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100;
|
|
||||||
}
|
|
||||||
.badge-info {
|
|
||||||
@apply bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100;
|
|
||||||
}
|
|
||||||
.table-container {
|
|
||||||
@apply overflow-hidden rounded-lg shadow-sm border border-secondary-200 dark:border-secondary-700;
|
|
||||||
}
|
|
||||||
.table {
|
|
||||||
@apply min-w-full divide-y divide-secondary-200 dark:divide-secondary-700;
|
|
||||||
}
|
|
||||||
.table thead {
|
|
||||||
@apply bg-secondary-50 dark:bg-secondary-900;
|
|
||||||
}
|
|
||||||
.table th {
|
|
||||||
@apply px-6 py-3 text-left text-xs font-medium text-secondary-500 uppercase tracking-wider dark:text-secondary-400;
|
|
||||||
}
|
|
||||||
.table tbody {
|
|
||||||
@apply bg-white dark:bg-secondary-800 divide-y divide-secondary-200 dark:divide-secondary-700;
|
|
||||||
}
|
|
||||||
.table td {
|
|
||||||
@apply px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-secondary-100;
|
|
||||||
}
|
|
||||||
.nav-link {
|
|
||||||
@apply inline-flex items-center px-1 pt-1 text-secondary-500 hover:text-secondary-700 dark:text-secondary-400 dark:hover:text-secondary-200 border-b-2 border-transparent hover:border-primary-500 transition-all duration-200;
|
|
||||||
}
|
|
||||||
.nav-link-active {
|
|
||||||
@apply inline-flex items-center px-1 pt-1 text-primary-600 dark:text-primary-400 border-b-2 border-primary-500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode styles */
|
|
||||||
.dark body {
|
|
||||||
@apply bg-secondary-900 text-secondary-100;
|
|
||||||
}
|
|
||||||
.dark .bg-white {
|
|
||||||
@apply bg-secondary-800;
|
|
||||||
}
|
|
||||||
.dark .text-secondary-800 {
|
|
||||||
@apply text-secondary-100;
|
|
||||||
}
|
|
||||||
.dark .text-secondary-700 {
|
|
||||||
@apply text-secondary-200;
|
|
||||||
}
|
|
||||||
.dark .text-secondary-500 {
|
|
||||||
@apply text-secondary-300;
|
|
||||||
}
|
|
||||||
.dark .hover\:text-secondary-700:hover {
|
|
||||||
@apply text-secondary-100;
|
|
||||||
}
|
|
||||||
.dark .hover\:bg-secondary-100:hover {
|
|
||||||
@apply bg-secondary-700;
|
|
||||||
}
|
|
||||||
.dark .bg-secondary-50 {
|
|
||||||
@apply bg-secondary-900;
|
|
||||||
}
|
|
||||||
.dark .bg-secondary-200 {
|
|
||||||
@apply bg-secondary-700;
|
|
||||||
}
|
|
||||||
.dark .shadow-sm {
|
|
||||||
@apply shadow-secondary-900;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Additional dark mode improvements */
|
|
||||||
.dark .bg-secondary-50 {
|
|
||||||
@apply bg-secondary-900;
|
|
||||||
}
|
|
||||||
.dark .bg-secondary-100 {
|
|
||||||
@apply bg-secondary-800;
|
|
||||||
}
|
|
||||||
.dark .border-secondary-200 {
|
|
||||||
@apply border-secondary-700;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom animations */
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; }
|
from { opacity: 0; }
|
||||||
to { opacity: 1; }
|
to { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-fadeIn {
|
.animate-fadeIn {
|
||||||
animation: fadeIn 0.3s ease-in-out;
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbar */
|
.pb-mobile-nav {
|
||||||
::-webkit-scrollbar {
|
padding-bottom: 4rem;
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: theme('colors.secondary.100');
|
@media (min-width: 640px) {
|
||||||
border-radius: 4px;
|
.pb-mobile-nav {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.dark ::-webkit-scrollbar-track {
|
|
||||||
background: theme('colors.secondary.800');
|
/* Ensure dark mode background extends to full height */
|
||||||
|
body.dark {
|
||||||
|
background-color: #111827; /* gray-900 */
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: theme('colors.secondary.300');
|
/* Force dark background on specific pages */
|
||||||
border-radius: 4px;
|
body.dark .configs-page,
|
||||||
|
body.dark .jobs-page {
|
||||||
|
background-color: #111827 !important; /* gray-900 */
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
.dark ::-webkit-scrollbar-thumb {
|
|
||||||
background: theme('colors.secondary.600');
|
/* Ensure dark mode fills entire page */
|
||||||
|
body.dark {
|
||||||
|
background-color: #111827 !important;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: theme('colors.secondary.500');
|
/* Force container backgrounds */
|
||||||
|
#jobs-container.dark, #configs-container.dark {
|
||||||
|
background-color: #111827 !important;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
.dark ::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: theme('colors.secondary.500');
|
/* Override any white backgrounds in dark mode */
|
||||||
|
body.dark .bg-white {
|
||||||
|
background-color: #1f2937 !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen flex flex-col bg-secondary-50 dark:bg-secondary-900 h-full p-0 m-0">
|
<body class="min-h-full bg-gray-50 dark:bg-gray-900" style="min-height: 100vh; display: flex; flex-direction: column;" hx-on::after-swap="initFlowbite()">
|
||||||
if isLoggedIn(ctx) {
|
if isLoggedIn(ctx) {
|
||||||
<nav class="bg-white dark:bg-secondary-800 shadow-sm sticky top-0 z-10 w-full">
|
<!-- Application Shell -->
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="flex min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
<div class="flex justify-between h-16">
|
<!-- Desktop Sidebar -->
|
||||||
<div class="flex">
|
<aside class="hidden md:flex md:w-64 md:flex-col md:fixed md:inset-y-0">
|
||||||
|
<div class="flex flex-col flex-grow pt-5 bg-white dark:bg-gray-800 overflow-y-auto border-r border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="flex items-center flex-shrink-0 px-4">
|
||||||
<a href="/" class="flex items-center text-xl font-bold text-primary-600 dark:text-primary-400">
|
<a href="/" class="flex items-center text-xl font-bold text-primary-600 dark:text-primary-400">
|
||||||
<i class="fas fa-exchange-alt mr-2"></i>
|
<i class="fas fa-exchange-alt mr-2"></i>
|
||||||
GoMFT
|
GoMFT
|
||||||
</a>
|
</a>
|
||||||
<div class="hidden sm:ml-6 sm:flex sm:space-x-8">
|
</div>
|
||||||
<a href="/dashboard" class="nav-link">
|
<nav class="flex-1 px-2 py-4 bg-white dark:bg-gray-800">
|
||||||
<i class="fas fa-tachometer-alt mr-1"></i> Dashboard
|
<div class="space-y-1">
|
||||||
|
<a href="/dashboard" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
<i class="fas fa-tachometer-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Dashboard
|
||||||
</a>
|
</a>
|
||||||
<a href="/configs" class="nav-link">
|
<a href="/configs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-cogs mr-1"></i> Configs
|
<i class="fas fa-cogs mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Configs
|
||||||
</a>
|
</a>
|
||||||
<a href="/jobs" class="nav-link">
|
<a href="/jobs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-tasks mr-1"></i> Jobs
|
<i class="fas fa-tasks mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Jobs
|
||||||
</a>
|
</a>
|
||||||
<a href="/history" class="nav-link">
|
<a href="/history" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-history mr-1"></i> History
|
<i class="fas fa-history mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
History
|
||||||
</a>
|
</a>
|
||||||
<a href="/files" class="nav-link">
|
<a href="/files" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-file-alt mr-1"></i> Files
|
<i class="fas fa-file-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Files
|
||||||
</a>
|
</a>
|
||||||
if isAdmin(ctx) {
|
if isAdmin(ctx) {
|
||||||
<a href="/admin/users" class="nav-link">
|
<p class="px-3 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
<i class="fas fa-users mr-1"></i> Users
|
Administration
|
||||||
|
</p>
|
||||||
|
<a href="/admin/users" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
<i class="fas fa-users w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Users
|
||||||
</a>
|
</a>
|
||||||
<a href="/admin/tools" class="nav-link">
|
<a href="/admin/roles" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-tools mr-1"></i> Admin Tools
|
<i class="fas fa-user-shield w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Roles
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/admin/audit" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
<i class="fas fa-clipboard-list w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Audit Logs
|
||||||
|
</a>
|
||||||
|
<a href="/admin/database" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
<i class="fas fa-database w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Database Tools
|
||||||
|
</a>
|
||||||
|
// Settings Dropdown
|
||||||
|
<button type="button" class="flex items-center w-full p-2 text-base text-gray-900 transition duration-75 rounded-lg group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700" aria-controls="dropdown-settings" data-collapse-toggle="dropdown-settings">
|
||||||
|
<i class="fas fa-cog w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
<span class="flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">Settings</span>
|
||||||
|
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<ul id="dropdown-settings" class="hidden py-2 space-y-2">
|
||||||
|
// <li>
|
||||||
|
// <a href="#" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">General (Coming Soon)</a>
|
||||||
|
// </li>
|
||||||
|
<li>
|
||||||
|
<a href="/admin/settings/auth-providers" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
<i class="fas fa-user-lock w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Authentication Providers
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/admin/settings/notifications" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
<i class="fas fa-bell w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Notification Services
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
<div class="flex items-center space-x-4">
|
</div>
|
||||||
<!-- Theme Toggle -->
|
</aside>
|
||||||
<button
|
<!-- Mobile menu -->
|
||||||
id="theme-toggle"
|
<div class="md:hidden fixed top-0 z-30 w-full">
|
||||||
type="button"
|
<div class="bg-white dark:bg-gray-800 shadow-sm">
|
||||||
class="text-secondary-500 dark:text-secondary-400 hover:bg-secondary-100 dark:hover:bg-secondary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 rounded-lg text-sm p-2"
|
<div class="flex items-center justify-between px-4 py-3">
|
||||||
onclick="toggleTheme()"
|
<a href="/" class="flex items-center text-primary-600 dark:text-primary-400">
|
||||||
>
|
<i class="fas fa-exchange-alt text-xl"></i>
|
||||||
<i class="fas fa-sun hidden dark:block"></i>
|
</a>
|
||||||
<i class="fas fa-moon block dark:hidden"></i>
|
<div class="flex items-center space-x-3">
|
||||||
</button>
|
<button data-drawer-target="mobile-menu" data-drawer-toggle="mobile-menu" class="p-2 text-gray-500 rounded-lg hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700">
|
||||||
|
|
||||||
<!-- Mobile menu button -->
|
|
||||||
<div class="sm:hidden flex items-center">
|
|
||||||
<button
|
|
||||||
id="mobile-menu-button"
|
|
||||||
class="text-secondary-500 dark:text-secondary-400 hover:bg-secondary-100 dark:hover:bg-secondary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 rounded-lg text-sm p-2"
|
|
||||||
x-data="{}"
|
|
||||||
@click="$dispatch('toggle-mobile-menu')"
|
|
||||||
>
|
|
||||||
<i class="fas fa-bars text-lg"></i>
|
<i class="fas fa-bars text-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- User Menu -->
|
|
||||||
<div x-data="{ open: false }" class="relative">
|
|
||||||
<button @click="open = !open" class="flex text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
|
|
||||||
<span class="sr-only">Open user menu</span>
|
|
||||||
<div class="h-8 w-8 rounded-full bg-primary-100 text-primary-700 flex items-center justify-center dark:bg-primary-900 dark:text-primary-300">
|
|
||||||
<span class="text-sm font-medium">{ getUserInitial(ctx) }</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<div x-show="open"
|
|
||||||
@click.away="open = false"
|
|
||||||
class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-secondary-800 dark:ring-secondary-700 animate-fadeIn"
|
|
||||||
role="menu"
|
|
||||||
aria-orientation="vertical"
|
|
||||||
aria-labelledby="user-menu-button"
|
|
||||||
tabindex="-1">
|
|
||||||
<div class="px-4 py-2 text-xs text-secondary-500 dark:text-secondary-400 border-b border-secondary-200 dark:border-secondary-700">
|
|
||||||
Signed in as <span class="font-medium">{ getUserEmail(ctx) }</span>
|
|
||||||
</div>
|
|
||||||
<a href="/profile" class="block px-4 py-2 text-sm text-secondary-700 hover:bg-secondary-100 dark:text-secondary-200 dark:hover:bg-secondary-700" role="menuitem">
|
|
||||||
<i class="fas fa-user-circle mr-2"></i> Profile
|
|
||||||
</a>
|
|
||||||
<form method="POST" action="/logout">
|
|
||||||
<button type="submit" class="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-secondary-100 dark:hover:bg-secondary-700" role="menuitem">
|
|
||||||
<i class="fas fa-sign-out-alt mr-2"></i> Sign out
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
<!-- Add this drawer element for mobile menu -->
|
||||||
|
<div id="mobile-menu" class="fixed top-0 left-0 z-40 h-screen p-4 overflow-y-auto transition-transform -translate-x-full bg-white w-64 dark:bg-gray-800" tabindex="-1">
|
||||||
<!-- Mobile Navigation Menu -->
|
<div class="flex items-center justify-between">
|
||||||
<div
|
<a href="/" class="flex items-center text-xl font-bold text-primary-600 dark:text-primary-400">
|
||||||
x-data="{ open: false }"
|
<i class="fas fa-exchange-alt mr-2"></i>
|
||||||
x-on:toggle-mobile-menu.window="open = !open"
|
GoMFT
|
||||||
x-show="open"
|
|
||||||
x-transition:enter="transition ease-out duration-200"
|
|
||||||
x-transition:enter-start="opacity-0 transform -translate-y-2"
|
|
||||||
x-transition:enter-end="opacity-100 transform translate-y-0"
|
|
||||||
x-transition:leave="transition ease-in duration-150"
|
|
||||||
x-transition:leave-start="opacity-100 transform translate-y-0"
|
|
||||||
x-transition:leave-end="opacity-0 transform -translate-y-2"
|
|
||||||
class="sm:hidden bg-white dark:bg-secondary-800 shadow-md w-full left-0 right-0"
|
|
||||||
>
|
|
||||||
<div class="max-w-7xl mx-auto px-2 pt-2 pb-3 space-y-1">
|
|
||||||
<a href="/dashboard" class="block px-3 py-2 rounded-md text-base font-medium text-secondary-700 dark:text-secondary-300 hover:bg-primary-50 dark:hover:bg-secondary-700 hover:text-primary-600 dark:hover:text-primary-400">
|
|
||||||
<i class="fas fa-tachometer-alt mr-2"></i> Dashboard
|
|
||||||
</a>
|
|
||||||
<a href="/configs" class="block px-3 py-2 rounded-md text-base font-medium text-secondary-700 dark:text-secondary-300 hover:bg-primary-50 dark:hover:bg-secondary-700 hover:text-primary-600 dark:hover:text-primary-400">
|
|
||||||
<i class="fas fa-cogs mr-2"></i> Configs
|
|
||||||
</a>
|
|
||||||
<a href="/jobs" class="block px-3 py-2 rounded-md text-base font-medium text-secondary-700 dark:text-secondary-300 hover:bg-primary-50 dark:hover:bg-secondary-700 hover:text-primary-600 dark:hover:text-primary-400">
|
|
||||||
<i class="fas fa-tasks mr-2"></i> Jobs
|
|
||||||
</a>
|
|
||||||
<a href="/history" class="block px-3 py-2 rounded-md text-base font-medium text-secondary-700 dark:text-secondary-300 hover:bg-primary-50 dark:hover:bg-secondary-700 hover:text-primary-600 dark:hover:text-primary-400">
|
|
||||||
<i class="fas fa-history mr-2"></i> History
|
|
||||||
</a>
|
|
||||||
<a href="/files" class="block px-3 py-2 rounded-md text-base font-medium text-secondary-700 dark:text-secondary-300 hover:bg-primary-50 dark:hover:bg-secondary-700 hover:text-primary-600 dark:hover:text-primary-400">
|
|
||||||
<i class="fas fa-file-alt mr-2"></i> Files
|
|
||||||
</a>
|
|
||||||
if isAdmin(ctx) {
|
|
||||||
<a href="/admin/users" class="block px-3 py-2 rounded-md text-base font-medium text-secondary-700 dark:text-secondary-300 hover:bg-primary-50 dark:hover:bg-secondary-700 hover:text-primary-600 dark:hover:text-primary-400">
|
|
||||||
<i class="fas fa-users mr-2"></i> Users
|
|
||||||
</a>
|
</a>
|
||||||
<a href="/admin/tools" class="block px-3 py-2 rounded-md text-base font-medium text-secondary-700 dark:text-secondary-300 hover:bg-primary-50 dark:hover:bg-secondary-700 hover:text-primary-600 dark:hover:text-primary-400">
|
<button type="button" data-drawer-hide="mobile-menu" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white">
|
||||||
<i class="fas fa-tools mr-2"></i> Admin Tools
|
<i class="fas fa-times"></i>
|
||||||
</a>
|
|
||||||
}
|
|
||||||
<a href="/profile" class="block px-3 py-2 rounded-md text-base font-medium text-secondary-700 dark:text-secondary-300 hover:bg-primary-50 dark:hover:bg-secondary-700 hover:text-primary-600 dark:hover:text-primary-400">
|
|
||||||
<i class="fas fa-user-circle mr-2"></i> Profile
|
|
||||||
</a>
|
|
||||||
<form method="POST" action="/logout">
|
|
||||||
<button type="submit" class="w-full text-left block px-3 py-2 rounded-md text-base font-medium text-red-600 hover:bg-red-50 dark:hover:bg-red-900 hover:text-red-700 dark:hover:text-red-400">
|
|
||||||
<i class="fas fa-sign-out-alt mr-2"></i> Sign out
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
<!-- Copy the same navigation items from the desktop sidebar -->
|
||||||
</div>
|
<nav class="mt-5">
|
||||||
}
|
<div class="space-y-1">
|
||||||
<main class="flex-grow w-full max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8 animate-fadeIn pb-mobile-nav">
|
<!-- Copy the same links from the desktop sidebar -->
|
||||||
<!-- Hidden notification area for HTMX targets -->
|
<a href="/dashboard" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<div id="notification-area" class="hidden" hx-swap-oob="true"></div>
|
<i class="fas fa-tachometer-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Dashboard
|
||||||
{ children... }
|
</a>
|
||||||
</main>
|
<a href="/configs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<footer class="bg-white dark:bg-secondary-800 shadow-inner mt-auto w-full">
|
<i class="fas fa-cogs mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
<div class="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
|
Configs
|
||||||
<p class="text-center text-sm text-secondary-500 dark:text-secondary-400">
|
</a>
|
||||||
GoMFT © { getCurrentYear() } | Secure File Transfer Solution
|
<a href="/jobs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
</p>
|
<i class="fas fa-tasks mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
<div class="flex justify-center items-center space-x-4 mt-1">
|
Jobs
|
||||||
<p class="text-xs text-secondary-400 dark:text-secondary-500">
|
</a>
|
||||||
<a href="https://github.com/starfleetcptn/gomft" class="hover:text-primary-600 dark:hover:text-primary-400 transition-colors duration-200" target="_blank" rel="noopener">
|
<a href="/history" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fab fa-github mr-1"></i>GitHub
|
<i class="fas fa-history mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
</a>
|
History
|
||||||
</p>
|
</a>
|
||||||
<p class="text-xs text-secondary-400 dark:text-secondary-500">
|
<a href="/files" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<a href={ GetReleaseURL() } class="hover:text-primary-600 dark:hover:text-primary-400 transition-colors duration-200" target="_blank" rel="noopener">
|
<i class="fas fa-file-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
<i class="fas fa-tag mr-1"></i>Version { AppVersion }
|
Files
|
||||||
</a>
|
</a>
|
||||||
</p>
|
if isAdmin(ctx) {
|
||||||
</div>
|
<p class="px-3 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
</div>
|
Administration
|
||||||
</footer>
|
</p>
|
||||||
|
<a href="/admin/users" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
if isLoggedIn(ctx) {
|
<i class="fas fa-users w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
<!-- Mobile Bottom Navigation -->
|
Users
|
||||||
<div class="sm:hidden fixed bottom-0 left-0 right-0 bg-white dark:bg-secondary-800 border-t border-secondary-200 dark:border-secondary-700 shadow-lg w-full p-0 m-0">
|
</a>
|
||||||
<div class="grid grid-cols-6 h-16">
|
<a href="/admin/roles" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<a href="/dashboard" class="flex flex-col items-center justify-center text-secondary-500 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400">
|
<i class="fas fa-user-shield w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
<i class="fas fa-tachometer-alt text-lg"></i>
|
Roles
|
||||||
<span class="text-xs mt-1">Dashboard</span>
|
</a>
|
||||||
</a>
|
<a href="/admin/audit" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<a href="/configs" class="flex flex-col items-center justify-center text-secondary-500 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400">
|
<i class="fas fa-clipboard-list w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
<i class="fas fa-cogs text-lg"></i>
|
Audit Logs
|
||||||
<span class="text-xs mt-1">Configs</span>
|
</a>
|
||||||
</a>
|
<a href="/admin/database" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<a href="/jobs" class="flex flex-col items-center justify-center text-secondary-500 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400">
|
<i class="fas fa-database w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
<i class="fas fa-tasks text-lg"></i>
|
Database Tools
|
||||||
<span class="text-xs mt-1">Jobs</span>
|
</a>
|
||||||
</a>
|
// Settings Dropdown
|
||||||
<a href="/history" class="flex flex-col items-center justify-center text-secondary-500 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400">
|
<button type="button" class="flex items-center w-full p-2 text-base text-gray-900 transition duration-75 rounded-lg group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700" aria-controls="dropdown-settings" data-collapse-toggle="dropdown-settings">
|
||||||
<i class="fas fa-history text-lg"></i>
|
<i class="fas fa-cog w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
<span class="text-xs mt-1">History</span>
|
<span class="flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">Settings</span>
|
||||||
</a>
|
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
||||||
<a href="/files" class="flex flex-col items-center justify-center text-secondary-500 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400">
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
|
||||||
<i class="fas fa-file-alt text-lg"></i>
|
</svg>
|
||||||
<span class="text-xs mt-1">Files</span>
|
</button>
|
||||||
</a>
|
<ul id="dropdown-settings" class="hidden py-2 space-y-2">
|
||||||
<button
|
// <li>
|
||||||
type="button"
|
// <a href="#" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">General (Coming Soon)</a>
|
||||||
class="flex flex-col items-center justify-center text-secondary-500 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400"
|
// </li>
|
||||||
x-data="{}"
|
<li>
|
||||||
@click="$dispatch('toggle-mobile-menu')"
|
<a href="/admin/settings/auth-providers" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
>
|
<i class="fas fa-user-lock w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
<i class="fas fa-ellipsis-h text-lg"></i>
|
Authentication Providers
|
||||||
<span class="text-xs mt-1">More</span>
|
</a>
|
||||||
</button>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/admin/settings/notifications" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
<i class="fas fa-bell w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Notification Services
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="md:pl-64 flex flex-col flex-1 w-full bg-gray-50 dark:bg-gray-900">
|
||||||
|
<!-- Main header -->
|
||||||
|
<header class="bg-white dark:bg-gray-800 shadow-sm">
|
||||||
|
<div class="flex items-center justify-between px-4 py-3 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<h1 class="text-lg font-semibold text-gray-900 dark:text-white">{ title }</h1>
|
||||||
|
</div>
|
||||||
|
<!-- Theme toggle and user menu -->
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<!-- Theme Toggle -->
|
||||||
|
<button
|
||||||
|
id="theme-toggle"
|
||||||
|
type="button"
|
||||||
|
class="p-2 text-gray-500 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-700 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600"
|
||||||
|
onclick="toggleTheme()"
|
||||||
|
>
|
||||||
|
<i class="fas fa-sun hidden dark:block"></i>
|
||||||
|
<i class="fas fa-moon block dark:hidden"></i>
|
||||||
|
</button>
|
||||||
|
<!-- Notification Bell -->
|
||||||
|
<div class="relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="p-2 text-gray-500 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-700 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600 relative"
|
||||||
|
id="notification-bell"
|
||||||
|
data-dropdown-toggle="notification-dropdown"
|
||||||
|
hx-get="/notifications/dropdown"
|
||||||
|
hx-trigger="click once"
|
||||||
|
hx-target="#notification-dropdown-content"
|
||||||
|
data-dropdown-placement="bottom-end"
|
||||||
|
>
|
||||||
|
<span class="sr-only">View notifications</span>
|
||||||
|
<i class="fas fa-bell"></i>
|
||||||
|
<!-- Notification badge will be loaded dynamically -->
|
||||||
|
<div hx-get="/notifications/count" hx-trigger="load, notification-updated from:body" id="notification-count-container"></div>
|
||||||
|
</button>
|
||||||
|
<!-- Notification dropdown -->
|
||||||
|
<div
|
||||||
|
class="hidden overflow-hidden z-50 my-4 max-w-sm md:max-w-md w-full text-base list-none bg-white rounded divide-y divide-gray-100 shadow-lg dark:divide-gray-600 dark:bg-gray-700 rounded-xl"
|
||||||
|
id="notification-dropdown"
|
||||||
|
style="min-width: 320px; width: 100%;"
|
||||||
|
data-dropdown-placement="bottom-end"
|
||||||
|
>
|
||||||
|
<div id="notification-dropdown-content">
|
||||||
|
<!-- Content will be loaded dynamically via HTMX -->
|
||||||
|
<div class="block py-2 px-4 text-base font-medium text-center text-gray-700 bg-gray-50 dark:bg-gray-600 dark:text-gray-300">
|
||||||
|
Notifications
|
||||||
|
</div>
|
||||||
|
<div class="py-4 px-4 text-center text-gray-500 dark:text-gray-400">
|
||||||
|
<div class="animate-pulse flex flex-col items-center">
|
||||||
|
<div class="rounded-full bg-gray-200 dark:bg-gray-700 h-12 w-12 mb-2"></div>
|
||||||
|
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-4"></div>
|
||||||
|
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-full mb-2"></div>
|
||||||
|
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-full mb-2"></div>
|
||||||
|
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="/notifications" class="block py-2 text-md font-medium text-center text-gray-900 bg-gray-50 hover:bg-gray-100 dark:bg-gray-600 dark:text-white dark:hover:underline">
|
||||||
|
<div class="inline-flex items-center">
|
||||||
|
<svg aria-hidden="true" class="mr-2 w-4 h-4 text-gray-500 dark:text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"></path>
|
||||||
|
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
View all
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- User menu dropdown -->
|
||||||
|
<div class="relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex text-sm rounded-full focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600"
|
||||||
|
id="user-menu-button"
|
||||||
|
data-dropdown-toggle="user-dropdown"
|
||||||
|
data-dropdown-placement="bottom-end"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open user menu</span>
|
||||||
|
<div class="h-8 w-8 rounded-full bg-primary-100 text-primary-700 flex items-center justify-center dark:bg-primary-900 dark:text-primary-300">
|
||||||
|
<span class="text-sm font-medium">{ getUserInitial(ctx) }</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<!-- Dropdown menu -->
|
||||||
|
<div
|
||||||
|
class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600"
|
||||||
|
id="user-dropdown"
|
||||||
|
>
|
||||||
|
<div class="px-4 py-3">
|
||||||
|
<span class="block text-sm text-gray-900 dark:text-white">{ getUserEmail(ctx) }</span>
|
||||||
|
</div>
|
||||||
|
<ul class="py-2" aria-labelledby="user-menu-button">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="/profile"
|
||||||
|
class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
|
||||||
|
>
|
||||||
|
<i class="fas fa-user-circle mr-2 w-4"></i>
|
||||||
|
Profile
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<form method="POST" action="/logout">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full text-left flex items-center px-4 py-2 text-sm text-red-600 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||||
|
>
|
||||||
|
<i class="fas fa-sign-out-alt mr-2 w-4"></i>
|
||||||
|
Sign out
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<!-- Toast Container -->
|
||||||
|
@toast.Container()
|
||||||
|
<!-- Page Content -->
|
||||||
|
<main class="flex-1 bg-gray-50 dark:bg-gray-900">
|
||||||
|
<div class="py-6 bg-gray-50 dark:bg-gray-900">
|
||||||
|
<div class="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<!-- Notification area -->
|
||||||
|
<div id="notification-area" class="hidden" hx-swap-oob="true"></div>
|
||||||
|
{ children... }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="max-w-screen-2xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<span>GoMFT © { getCurrentYear() }</span>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<a href="https://github.com/starfleetcptn/gomft" class="hover:text-primary-600 dark:hover:text-primary-400">
|
||||||
|
<i class="fab fa-github"></i>
|
||||||
|
</a>
|
||||||
|
<a href={ GetReleaseURL() } class="hover:text-primary-600 dark:hover:text-primary-400">
|
||||||
|
<i class="fas fa-tag"></i> { AppVersion }
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<!-- Public page layout -->
|
||||||
|
<div class="flex flex-col min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
|
<main class="flex-grow">
|
||||||
|
{ children... }
|
||||||
|
</main>
|
||||||
|
<footer class="bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="max-w-screen-2xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<span>GoMFT © { getCurrentYear() }</span>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<a href="https://github.com/starfleetcptn/gomft" class="hover:text-primary-600 dark:hover:text-primary-400">
|
||||||
|
<i class="fab fa-github"></i>
|
||||||
|
</a>
|
||||||
|
<a href={ GetReleaseURL() } class="hover:text-primary-600 dark:hover:text-primary-400">
|
||||||
|
<i class="fas fa-tag"></i> { AppVersion }
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
<!-- Scripts -->
|
||||||
|
<!-- Alpine.js and dependencies -->
|
||||||
|
<script defer src="/static/dist/vendor.js"></script>
|
||||||
|
<!-- Shared Scripts -->
|
||||||
|
@toast.ShowToastJS()
|
||||||
|
<!-- Application scripts -->
|
||||||
|
<script defer src="/static/dist/app.js"></script>
|
||||||
|
<script defer src="/static/dist/init.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
}
|
}
|
||||||
@@ -536,17 +542,17 @@ func isLoggedIn(ctx context.Context) bool {
|
|||||||
func getUserInitial(ctx context.Context) string {
|
func getUserInitial(ctx context.Context) string {
|
||||||
// Try as string first
|
// Try as string first
|
||||||
if username, ok := ctx.Value("username").(string); ok && username != "" {
|
if username, ok := ctx.Value("username").(string); ok && username != "" {
|
||||||
return string(username[0])
|
return strings.ToUpper(string(username[0]))
|
||||||
}
|
}
|
||||||
// Try as interface{} (from JWT claims)
|
// Try as interface{} (from JWT claims)
|
||||||
if username, ok := ctx.Value("username").(interface{}); ok {
|
if username, ok := ctx.Value("username").(interface{}); ok {
|
||||||
if strVal, ok := username.(string); ok && strVal != "" {
|
if strVal, ok := username.(string); ok && strVal != "" {
|
||||||
return string(strVal[0])
|
return strings.ToUpper(string(strVal[0]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Try email as fallback
|
// Try email as fallback
|
||||||
if email, ok := ctx.Value("email").(string); ok && email != "" {
|
if email, ok := ctx.Value("email").(string); ok && email != "" {
|
||||||
return string(email[0])
|
return strings.ToUpper(string(email[0]))
|
||||||
}
|
}
|
||||||
return "U"
|
return "U"
|
||||||
}
|
}
|
||||||
@@ -555,12 +561,12 @@ func getUserInitial(ctx context.Context) string {
|
|||||||
func getUserEmail(ctx context.Context) string {
|
func getUserEmail(ctx context.Context) string {
|
||||||
// Try as string first
|
// Try as string first
|
||||||
if email, ok := ctx.Value("email").(string); ok && email != "" {
|
if email, ok := ctx.Value("email").(string); ok && email != "" {
|
||||||
return email
|
return strings.ToLower(email)
|
||||||
}
|
}
|
||||||
// Try as interface{} (from JWT claims)
|
// Try as interface{} (from JWT claims)
|
||||||
if email, ok := ctx.Value("email").(interface{}); ok {
|
if email, ok := ctx.Value("email").(interface{}); ok {
|
||||||
if strVal, ok := email.(string); ok && strVal != "" {
|
if strVal, ok := email.(string); ok && strVal != "" {
|
||||||
return strVal
|
return strings.ToLower(strVal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "user@example.com"
|
return "user@example.com"
|
||||||
@@ -569,4 +575,4 @@ func getUserEmail(ctx context.Context) string {
|
|||||||
// Helper function to get current year
|
// Helper function to get current year
|
||||||
func getCurrentYear() string {
|
func getCurrentYear() string {
|
||||||
return time.Now().Format("2006")
|
return time.Now().Format("2006")
|
||||||
}
|
}
|
||||||
|
|||||||
+114
-116
@@ -7,131 +7,129 @@ import (
|
|||||||
|
|
||||||
templ Login(ctx context.Context, errorMessage string) {
|
templ Login(ctx context.Context, errorMessage string) {
|
||||||
@LayoutWithContext("Login", ctx) {
|
@LayoutWithContext("Login", ctx) {
|
||||||
<div class="min-h-[calc(100vh-4rem)] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 bg-secondary-50 dark:bg-secondary-900">
|
<div class="min-h-[calc(100vh-4rem)] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 bg-gray-50 dark:bg-gray-900">
|
||||||
<div class="max-w-md w-full">
|
<div class="max-w-md w-full space-y-8">
|
||||||
<div class="card overflow-hidden shadow-lg">
|
<div class="bg-white dark:bg-gray-800 shadow-lg rounded-lg p-8">
|
||||||
<div class="p-8">
|
<div class="text-center mb-8">
|
||||||
<div class="text-center mb-8">
|
<div class="mx-auto mb-4 flex h-20 w-20 items-center justify-center rounded-full bg-primary-100 dark:bg-primary-900">
|
||||||
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-primary-100 dark:bg-primary-900 mb-4">
|
<i class="fas fa-lock text-primary-600 dark:text-primary-400 text-3xl"></i>
|
||||||
<i class="fas fa-lock text-primary-600 dark:text-primary-400 text-3xl"></i>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-3xl font-bold text-secondary-900 dark:text-secondary-100">Sign In</h2>
|
|
||||||
<p class="mt-2 text-secondary-600 dark:text-secondary-400">Access your GoMFT account</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h2 class="text-3xl font-bold text-gray-900 dark:text-white">Sign In</h2>
|
||||||
if errorMessage != "" {
|
<p class="mt-2 text-gray-600 dark:text-gray-300">Access your GoMFT account</p>
|
||||||
if strings.HasPrefix(errorMessage, "Password reset") || strings.Contains(errorMessage, "success") {
|
|
||||||
<div class="bg-green-100 dark:bg-green-900 border border-green-400 dark:border-green-700 text-green-700 dark:text-green-300 px-4 py-3 rounded-lg mb-6" role="alert">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<i class="fas fa-check-circle mr-2"></i>
|
|
||||||
<span class="block sm:inline">{ errorMessage }</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
<div class="bg-red-100 dark:bg-red-900 border border-red-400 dark:border-red-700 text-red-700 dark:text-red-300 px-4 py-3 rounded-lg mb-6" role="alert">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<i class="fas fa-exclamation-circle mr-2"></i>
|
|
||||||
<span class="block sm:inline">{ errorMessage }</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<form
|
|
||||||
class="space-y-6"
|
|
||||||
method="POST"
|
|
||||||
action="/login"
|
|
||||||
x-data="{
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
loading: false,
|
|
||||||
validate() {
|
|
||||||
return this.email && this.password;
|
|
||||||
}
|
|
||||||
}">
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label for="email" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Email address</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-envelope text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
x-model="email"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="you@example.com"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Password</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
x-model="password"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="••••••••"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<input
|
|
||||||
id="remember-me"
|
|
||||||
name="remember-me"
|
|
||||||
type="checkbox"
|
|
||||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 dark:border-secondary-700 rounded"/>
|
|
||||||
<label for="remember-me" class="ml-2 block text-sm text-secondary-700 dark:text-secondary-300">Remember me</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-sm">
|
|
||||||
<a href="/forgot-password" class="font-medium text-primary-600 dark:text-primary-400 hover:text-primary-500 dark:hover:text-primary-300">Forgot password?</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn-primary w-full flex justify-center py-3"
|
|
||||||
x-bind:disabled="!validate()"
|
|
||||||
@click="loading = true">
|
|
||||||
<span x-show="!loading" class="flex items-center">
|
|
||||||
<i class="fas fa-sign-in-alt mr-2"></i>
|
|
||||||
Sign in
|
|
||||||
</span>
|
|
||||||
<span x-show="loading" class="flex items-center">
|
|
||||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
Processing...
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-8 py-4 bg-secondary-50 dark:bg-secondary-800 border-t border-secondary-200 dark:border-secondary-700 text-center">
|
if errorMessage != "" {
|
||||||
<p class="text-sm text-secondary-600 dark:text-secondary-400">
|
if strings.HasPrefix(errorMessage, "Password reset") || strings.Contains(errorMessage, "success") {
|
||||||
|
<div class="mb-6 flex items-center rounded-lg bg-green-50 p-4 text-green-800 dark:bg-gray-800 dark:text-green-400" role="alert">
|
||||||
|
<i class="fas fa-check-circle mr-2"></i>
|
||||||
|
<span class="[&:not(:first-child)]:ms-2">{ errorMessage }</span>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="mb-6 flex items-center rounded-lg bg-red-50 p-4 text-red-800 dark:bg-gray-800 dark:text-red-400" role="alert">
|
||||||
|
<i class="fas fa-exclamation-circle mr-2"></i>
|
||||||
|
<span class="[&:not(:first-child)]:ms-2">{ errorMessage }</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="space-y-6"
|
||||||
|
method="POST"
|
||||||
|
action="/login"
|
||||||
|
x-data="{
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
loading: false,
|
||||||
|
validate() {
|
||||||
|
return this.email && this.password;
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="email" class="mb-2 block text-sm font-medium text-gray-900 dark:text-white">Email address</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
|
<i class="fas fa-envelope text-gray-500 dark:text-gray-400"></i>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
x-model="email"
|
||||||
|
class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 pl-10 text-gray-900 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500"
|
||||||
|
placeholder="you@example.com"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password" class="mb-2 block text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
|
<i class="fas fa-key text-gray-500 dark:text-gray-400"></i>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
x-model="password"
|
||||||
|
class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 pl-10 text-gray-900 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500"
|
||||||
|
placeholder="••••••••"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input
|
||||||
|
id="remember-me"
|
||||||
|
name="remember-me"
|
||||||
|
type="checkbox"
|
||||||
|
class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-primary-600 focus:ring-2 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800"/>
|
||||||
|
<label for="remember-me" class="ms-2 text-sm text-gray-900 dark:text-gray-300">Remember me</label>
|
||||||
|
</div>
|
||||||
|
<a href="/forgot-password" class="text-sm font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300">Forgot password?</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full rounded-lg bg-primary-600 px-5 py-3 text-center text-sm font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
|
||||||
|
x-bind:disabled="!validate()"
|
||||||
|
@click="loading = true">
|
||||||
|
<span x-show="!loading" class="flex items-center justify-center">
|
||||||
|
<i class="fas fa-sign-in-alt mr-2"></i>
|
||||||
|
Sign in
|
||||||
|
</span>
|
||||||
|
<span x-show="loading" class="flex items-center justify-center">
|
||||||
|
<svg class="me-3 h-5 w-5 animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
Processing...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-6 border-t border-gray-200 px-8 py-4 text-center dark:border-gray-700">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300 mb-3">
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
<i class="fas fa-info-circle mr-1"></i>
|
||||||
Contact an administrator to create an account
|
Contact an administrator to create an account
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- External Authentication Providers -->
|
||||||
|
<div id="external-auth-providers" class="mt-4">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300 mb-3">Or sign in with:</p>
|
||||||
|
<div id="provider-buttons" class="flex flex-col gap-2" hx-get="/auth/providers" hx-trigger="load" hx-target="#provider-buttons">
|
||||||
|
<div class="animate-pulse flex justify-center">
|
||||||
|
<div class="h-10 bg-gray-200 rounded w-full max-w-[200px] dark:bg-gray-700"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Security Notice -->
|
<div class="text-center">
|
||||||
<div class="mt-8 text-center">
|
<div class="inline-flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||||
<div class="inline-flex items-center text-sm text-secondary-500 dark:text-secondary-400">
|
|
||||||
<i class="fas fa-shield-alt mr-2 text-primary-500"></i>
|
<i class="fas fa-shield-alt mr-2 text-primary-500"></i>
|
||||||
<span>Secure, encrypted connection</span>
|
<span>Secure, encrypted connection</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,985 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationFormData struct {
|
||||||
|
NotificationService *struct {
|
||||||
|
ID uint
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Type string
|
||||||
|
IsEnabled bool
|
||||||
|
EventTriggers []string
|
||||||
|
RetryPolicy string
|
||||||
|
WebhookURL string
|
||||||
|
Method string
|
||||||
|
Headers string
|
||||||
|
PayloadTemplate string
|
||||||
|
SecretKey string
|
||||||
|
PushbulletAPIKey string
|
||||||
|
PushbulletDeviceID string
|
||||||
|
PushbulletTitleTemplate string
|
||||||
|
PushbulletBodyTemplate string
|
||||||
|
NtfyServer string
|
||||||
|
NtfyTopic string
|
||||||
|
NtfyPriority string
|
||||||
|
NtfyUsername string
|
||||||
|
NtfyPassword string
|
||||||
|
NtfyTitleTemplate string
|
||||||
|
NtfyMessageTemplate string
|
||||||
|
GotifyURL string
|
||||||
|
GotifyToken string
|
||||||
|
GotifyPriority string
|
||||||
|
GotifyTitleTemplate string
|
||||||
|
GotifyMessageTemplate string
|
||||||
|
PushoverAPIToken string
|
||||||
|
PushoverUserKey string
|
||||||
|
PushoverDevice string
|
||||||
|
PushoverPriority string
|
||||||
|
PushoverSound string
|
||||||
|
PushoverTitleTemplate string
|
||||||
|
PushoverMessageTemplate string
|
||||||
|
}
|
||||||
|
IsNew bool
|
||||||
|
SuccessMessage string
|
||||||
|
ErrorMessage string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if a string is in a slice
|
||||||
|
func contains(slice []string, str string) bool {
|
||||||
|
for _, s := range slice {
|
||||||
|
if s == str {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to convert bool to string for HTML attributes
|
||||||
|
func boolToString(b bool) string {
|
||||||
|
if b {
|
||||||
|
return "true"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNotificationFormTitle(isNew bool) string {
|
||||||
|
if isNew {
|
||||||
|
return "Add Notification Service"
|
||||||
|
}
|
||||||
|
return "Edit Notification Service"
|
||||||
|
}
|
||||||
|
|
||||||
|
templ NotificationForm(ctx context.Context, data NotificationFormData) {
|
||||||
|
@LayoutWithContext(getNotificationFormTitle(data.IsNew), ctx) {
|
||||||
|
<!-- Status and Error Messages -->
|
||||||
|
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Notification system
|
||||||
|
function showToast(message, type) {
|
||||||
|
const toastContainer = document.getElementById('toast-container');
|
||||||
|
|
||||||
|
// Create toast element
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.id = 'toast-' + type + '-' + Date.now();
|
||||||
|
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
||||||
|
toast.role = 'alert';
|
||||||
|
|
||||||
|
// Set toast content based on type
|
||||||
|
let iconClass, bgColorClass, textColorClass;
|
||||||
|
|
||||||
|
if (type === 'success') {
|
||||||
|
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
||||||
|
bgColorClass = 'text-green-500 dark:text-green-200';
|
||||||
|
textColorClass = 'text-green-500 dark:text-green-200';
|
||||||
|
} else if (type === 'error') {
|
||||||
|
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
||||||
|
bgColorClass = 'text-red-500 dark:text-red-200';
|
||||||
|
textColorClass = 'text-red-500 dark:text-red-200';
|
||||||
|
} else {
|
||||||
|
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
||||||
|
bgColorClass = 'text-blue-500 dark:text-blue-200';
|
||||||
|
textColorClass = 'text-blue-500 dark:text-blue-200';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set inner HTML with appropriate icon and message
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
||||||
|
${type === 'success'
|
||||||
|
? '<i class="fas fa-check"></i>'
|
||||||
|
: type === 'error'
|
||||||
|
? '<i class="fas fa-exclamation-circle"></i>'
|
||||||
|
: '<i class="fas fa-info-circle"></i>'}
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 text-sm font-normal">${message}</div>
|
||||||
|
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add toast to container
|
||||||
|
toastContainer.appendChild(toast);
|
||||||
|
|
||||||
|
// Trigger animation after a small delay to ensure the DOM has updated
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('translate-y-16', 'opacity-0');
|
||||||
|
toast.classList.add('translate-y-0', 'opacity-100');
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// Add event listener to close button
|
||||||
|
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
||||||
|
closeButton.addEventListener('click', function() {
|
||||||
|
// Animate out before removing
|
||||||
|
toast.classList.add('opacity-0', 'translate-y-4');
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-remove toast after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.add('opacity-0', 'translate-y-4');
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle notification fields based on selection
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const typeSelector = document.getElementById('notification_type');
|
||||||
|
const allFields = document.querySelectorAll('.notification-fields');
|
||||||
|
const commonFields = document.querySelectorAll('.common-fields');
|
||||||
|
|
||||||
|
typeSelector.addEventListener('change', function() {
|
||||||
|
// Hide all fields first
|
||||||
|
allFields.forEach(field => field.classList.add('hidden'));
|
||||||
|
|
||||||
|
// Show/hide common fields based on selection
|
||||||
|
const selectedType = this.value;
|
||||||
|
if (selectedType) {
|
||||||
|
// Show common fields (name, description)
|
||||||
|
commonFields.forEach(field => field.classList.remove('hidden'));
|
||||||
|
|
||||||
|
// Show the selected type's specific fields
|
||||||
|
const fieldsToShow = document.getElementById(`${selectedType}_fields`);
|
||||||
|
if (fieldsToShow) {
|
||||||
|
fieldsToShow.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Hide common fields if no type selected
|
||||||
|
commonFields.forEach(field => field.classList.add('hidden'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize form if editing
|
||||||
|
if (document.getElementById('notification_type').value) {
|
||||||
|
// Trigger the change event to show the appropriate fields
|
||||||
|
document.getElementById('notification_type').dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="notification-form-container" style="min-height: 100vh; background-color: rgb(249, 250, 251);" class="notifications-page bg-gray-50 dark:bg-gray-900">
|
||||||
|
<div class="pb-8 w-full max-w-4xl mx-auto">
|
||||||
|
<!-- Success Message (hidden, used for HTMX responses) -->
|
||||||
|
if data.SuccessMessage != "" {
|
||||||
|
<div class="hidden success-message">{ data.SuccessMessage }</div>
|
||||||
|
}
|
||||||
|
<!-- Error Message (hidden, used for HTMX responses) -->
|
||||||
|
if data.ErrorMessage != "" {
|
||||||
|
<div class="hidden error-message">{ data.ErrorMessage }</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-bell w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||||
|
{ getNotificationFormTitle(data.IsNew) }
|
||||||
|
</h1>
|
||||||
|
<a href="/admin/settings/notifications" class="flex items-center justify-center text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg px-5 py-2.5 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 focus:outline-none dark:focus:ring-gray-700">
|
||||||
|
<i class="fas fa-arrow-left w-4 h-4 mr-2"></i>
|
||||||
|
Back to Notification Services
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Notification Service Form -->
|
||||||
|
<div class="mb-6 p-6 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
<form id="notification-form"
|
||||||
|
if data.IsNew {
|
||||||
|
hx-post="/admin/settings/notifications"
|
||||||
|
} else {
|
||||||
|
hx-put={ fmt.Sprintf("/admin/settings/notifications/%d", data.NotificationService.ID) }
|
||||||
|
}
|
||||||
|
hx-target="body">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="notification_type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Notification Type</label>
|
||||||
|
<select id="notification_type" name="type" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option value="">Select a type</option>
|
||||||
|
if data.NotificationService != nil && data.NotificationService.Type == "webhook" {
|
||||||
|
<option value="webhook" selected="selected">Webhook</option>
|
||||||
|
} else {
|
||||||
|
<option value="webhook">Webhook</option>
|
||||||
|
}
|
||||||
|
if data.NotificationService != nil && data.NotificationService.Type == "pushbullet" {
|
||||||
|
<option value="pushbullet" selected="selected">Pushbullet</option>
|
||||||
|
} else {
|
||||||
|
<option value="pushbullet">Pushbullet</option>
|
||||||
|
}
|
||||||
|
if data.NotificationService != nil && data.NotificationService.Type == "ntfy" {
|
||||||
|
<option value="ntfy" selected="selected">Ntfy</option>
|
||||||
|
} else {
|
||||||
|
<option value="ntfy">Ntfy</option>
|
||||||
|
}
|
||||||
|
if data.NotificationService != nil && data.NotificationService.Type == "gotify" {
|
||||||
|
<option value="gotify" selected="selected">Gotify</option>
|
||||||
|
} else {
|
||||||
|
<option value="gotify">Gotify</option>
|
||||||
|
}
|
||||||
|
if data.NotificationService != nil && data.NotificationService.Type == "pushover" {
|
||||||
|
<option value="pushover" selected="selected">Pushover</option>
|
||||||
|
} else {
|
||||||
|
<option value="pushover">Pushover</option>
|
||||||
|
}
|
||||||
|
<option value="email" disabled>Email (Coming Soon)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6 hidden common-fields">
|
||||||
|
<label for="notification_name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Name</label>
|
||||||
|
if data.NotificationService.Name != "" {
|
||||||
|
<input type="text" id="notification_name" name="name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="My Notification Service" required value={ data.NotificationService.Name }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="notification_name" name="name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="My Notification Service" required value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6 hidden common-fields">
|
||||||
|
<label for="notification_description" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label>
|
||||||
|
if data.NotificationService.Description != "" {
|
||||||
|
<textarea id="notification_description" name="description" rows="3" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Description for this notification service">{ data.NotificationService.Description }</textarea>
|
||||||
|
} else {
|
||||||
|
<textarea id="notification_description" name="description" rows="3" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Description for this notification service"></textarea>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dynamic fields based on notification type -->
|
||||||
|
<div id="email_fields" class="hidden notification-fields">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="smtp_host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Host</label>
|
||||||
|
<input type="text" id="smtp_host" name="smtp_host" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="smtp.example.com"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="smtp_port" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Port</label>
|
||||||
|
<input type="number" id="smtp_port" name="smtp_port" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="587"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="smtp_username" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Username</label>
|
||||||
|
<input type="text" id="smtp_username" name="smtp_username" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="user@example.com"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="smtp_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Password</label>
|
||||||
|
<input type="password" id="smtp_password" name="smtp_password" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="from_email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">From Email</label>
|
||||||
|
<input type="email" id="from_email" name="from_email" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="notifications@example.com"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="webhook_fields" class="hidden notification-fields">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="webhook_url" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Webhook URL</label>
|
||||||
|
if data.NotificationService.WebhookURL != "" {
|
||||||
|
<input type="url" id="webhook_url" name="webhook_url" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://api.example.com/webhook" value={ data.NotificationService.WebhookURL }/>
|
||||||
|
} else {
|
||||||
|
<input type="url" id="webhook_url" name="webhook_url" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://api.example.com/webhook" value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="method" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">HTTP Method</label>
|
||||||
|
<select id="method" name="method" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
if data.NotificationService.Method != "" {
|
||||||
|
if data.NotificationService.Method == "POST" {
|
||||||
|
<option value="POST" selected="selected">POST</option>
|
||||||
|
} else {
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
}
|
||||||
|
if data.NotificationService.Method == "PUT" {
|
||||||
|
<option value="PUT" selected="selected">PUT</option>
|
||||||
|
} else {
|
||||||
|
<option value="PUT">PUT</option>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
<option value="PUT">PUT</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="headers" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Headers (JSON)</label>
|
||||||
|
if data.NotificationService.Headers != "" {
|
||||||
|
<textarea id="headers" name="headers" rows="3" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder='{"Content-Type": "application/json", "Authorization": "Bearer token"}'>{ data.NotificationService.Headers }</textarea>
|
||||||
|
} else {
|
||||||
|
<textarea id="headers" name="headers" rows="3" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder='{"Content-Type": "application/json", "Authorization": "Bearer token"}'></textarea>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="payload_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Payload Template (JSON)</label>
|
||||||
|
<textarea
|
||||||
|
id="payload_template"
|
||||||
|
name="payload_template"
|
||||||
|
rows="5"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder='{
|
||||||
|
"event": "{{job.event}}",
|
||||||
|
"job": {
|
||||||
|
"id": "{{job.id}}",
|
||||||
|
"name": "{{job.name}}",
|
||||||
|
"status": "{{job.status}}",
|
||||||
|
"message": "{{job.message}}",
|
||||||
|
"started_at": "{{job.started_at}}",
|
||||||
|
"completed_at": "{{job.completed_at}}",
|
||||||
|
"duration_seconds": {{job.duration_seconds}},
|
||||||
|
"config_id": "{{job.config_id}}",
|
||||||
|
"config_name": "{{job.config_name}}",
|
||||||
|
"transfer_bytes": {{job.transfer_bytes}},
|
||||||
|
"file_count": {{job.file_count}}
|
||||||
|
},
|
||||||
|
"instance": {
|
||||||
|
"id": "{{instance.id}}",
|
||||||
|
"name": "{{instance.name}}",
|
||||||
|
"version": "{{instance.version}}",
|
||||||
|
"environment": "{{instance.environment}}"
|
||||||
|
},
|
||||||
|
"timestamp": "{{timestamp}}",
|
||||||
|
"notification_id": "{{notification.id}}"
|
||||||
|
}'
|
||||||
|
>
|
||||||
|
if data.NotificationService.PayloadTemplate != "" {
|
||||||
|
data.NotificationService.PayloadTemplate
|
||||||
|
} </textarea>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Event Triggers</label>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center">
|
||||||
|
if data.NotificationService.EventTriggers != nil {
|
||||||
|
<input id="trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_start")) }/>
|
||||||
|
}
|
||||||
|
<label for="trigger_job_start" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Start</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
if data.NotificationService.EventTriggers != nil {
|
||||||
|
<input id="trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_complete")) }/>
|
||||||
|
}
|
||||||
|
<label for="trigger_job_complete" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Complete</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
if data.NotificationService.EventTriggers != nil {
|
||||||
|
<input id="trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_error")) }/>
|
||||||
|
}
|
||||||
|
<label for="trigger_job_error" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Error</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="secret_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Key (for signature verification)</label>
|
||||||
|
if data.NotificationService.SecretKey != "" {
|
||||||
|
<input type="text" id="secret_key" name="secret_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Optional signature verification key" value={ data.NotificationService.SecretKey }/>
|
||||||
|
}
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">If provided, all webhooks will include an X-GoMFT-Signature header</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="retry_policy" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Retry Policy</label>
|
||||||
|
<select id="retry_policy" name="retry_policy" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
if data.NotificationService.RetryPolicy != "" {
|
||||||
|
if data.NotificationService.RetryPolicy == "none" {
|
||||||
|
<option value="none" selected="selected">No retries</option>
|
||||||
|
} else {
|
||||||
|
<option value="none">No retries</option>
|
||||||
|
}
|
||||||
|
if data.NotificationService.RetryPolicy == "simple" {
|
||||||
|
<option value="simple" selected="selected">Simple (3 retries)</option>
|
||||||
|
} else {
|
||||||
|
<option value="simple">Simple (3 retries)</option>
|
||||||
|
}
|
||||||
|
if data.NotificationService.RetryPolicy == "exponential" {
|
||||||
|
<option value="exponential" selected="selected">Exponential backoff</option>
|
||||||
|
} else {
|
||||||
|
<option value="exponential">Exponential backoff</option>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
<option value="none">No retries</option>
|
||||||
|
<option value="simple">Simple (3 retries)</option>
|
||||||
|
<option value="exponential">Exponential backoff</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<!-- Test notification button -->
|
||||||
|
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="test-webhook-btn"
|
||||||
|
hx-post="/admin/settings/notifications/test"
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-target="#test-notification-result"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-paper-plane mr-1"></i>
|
||||||
|
Send Test Notification
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Send a test notification to verify your configuration works correctly before saving.
|
||||||
|
</p>
|
||||||
|
<div id="test-notification-result" class="mt-3 hidden">
|
||||||
|
<!-- Result will be shown here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pushbullet Fields -->
|
||||||
|
<div id="pushbullet_fields" class="hidden notification-fields">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushbullet_api_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">API Key</label>
|
||||||
|
if data.NotificationService.PushbulletAPIKey != "" {
|
||||||
|
<input type="text" id="pushbullet_api_key" name="pushbullet_api_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="o.XyzAbCdEfGhIjKlMnOpQrSt" value={ data.NotificationService.PushbulletAPIKey }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="pushbullet_api_key" name="pushbullet_api_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="o.XyzAbCdEfGhIjKlMnOpQrSt" value=""/>
|
||||||
|
}
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Get your API key from <a href="https://www.pushbullet.com/#settings/account" target="_blank" class="text-blue-500 hover:underline">Pushbullet Account Settings</a></p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushbullet_device_iden" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Device Identifier (Optional)</label>
|
||||||
|
if data.NotificationService.PushbulletDeviceID != "" {
|
||||||
|
<input type="text" id="pushbullet_device_iden" name="pushbullet_device_iden" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Leave empty to send to all devices" value={ data.NotificationService.PushbulletDeviceID }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="pushbullet_device_iden" name="pushbullet_device_iden" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Leave empty to send to all devices" value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushbullet_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Notification Title Template</label>
|
||||||
|
if data.NotificationService.PushbulletTitleTemplate != "" {
|
||||||
|
<input type="text" id="pushbullet_title_template" name="pushbullet_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" value={ data.NotificationService.PushbulletTitleTemplate }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="pushbullet_title_template" name="pushbullet_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="{{job.name}} {{job.status}}" value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushbullet_body_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
||||||
|
<textarea
|
||||||
|
id="pushbullet_body_template"
|
||||||
|
name="pushbullet_body_template"
|
||||||
|
rows="4"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
||||||
|
>
|
||||||
|
if data.NotificationService.PushbulletBodyTemplate != "" {
|
||||||
|
data.NotificationService.PushbulletBodyTemplate
|
||||||
|
} </textarea>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Event Triggers</label>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center">
|
||||||
|
if data.NotificationService.EventTriggers != nil {
|
||||||
|
<input id="pb_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_start")) }/>
|
||||||
|
} else {
|
||||||
|
<input id="pb_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||||
|
}
|
||||||
|
<label for="pb_trigger_job_start" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Start</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
if data.NotificationService.EventTriggers != nil {
|
||||||
|
<input id="pb_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_complete")) }/>
|
||||||
|
} else {
|
||||||
|
<input id="pb_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||||
|
}
|
||||||
|
<label for="pb_trigger_job_complete" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Complete</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
if data.NotificationService.EventTriggers != nil {
|
||||||
|
<input id="pb_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_error")) }/>
|
||||||
|
} else {
|
||||||
|
<input id="pb_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||||
|
}
|
||||||
|
<label for="pb_trigger_job_error" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Error</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Test notification button for Pushbullet -->
|
||||||
|
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="test-pushbullet-btn"
|
||||||
|
hx-post="/admin/settings/notifications/test"
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-target="#test-notification-result"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-paper-plane mr-1"></i>
|
||||||
|
Send Test Notification
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Send a test notification to verify your Pushbullet configuration works correctly before saving.
|
||||||
|
</p>
|
||||||
|
<div id="test-notification-result" class="mt-3 hidden">
|
||||||
|
<!-- Result will be shown here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ntfy Fields -->
|
||||||
|
<div id="ntfy_fields" class="hidden notification-fields">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="ntfy_server" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Ntfy Server</label>
|
||||||
|
if data.NotificationService.NtfyServer != "" {
|
||||||
|
<input type="url" id="ntfy_server" name="ntfy_server" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://ntfy.sh" value={ data.NotificationService.NtfyServer }/>
|
||||||
|
} else {
|
||||||
|
<input type="url" id="ntfy_server" name="ntfy_server" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://ntfy.sh" value="https://ntfy.sh"/>
|
||||||
|
}
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">The Ntfy server URL (default: ntfy.sh)</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="ntfy_topic" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Topic</label>
|
||||||
|
if data.NotificationService.NtfyTopic != "" {
|
||||||
|
<input type="text" id="ntfy_topic" name="ntfy_topic" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="your-unique-topic" value={ data.NotificationService.NtfyTopic }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="ntfy_topic" name="ntfy_topic" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="your-unique-topic" value="gomft"/>
|
||||||
|
}
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Choose a unique, unguessable topic name</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="ntfy_priority" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Default Priority</label>
|
||||||
|
<select id="ntfy_priority" name="ntfy_priority" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option value="1">Low (1)</option>
|
||||||
|
if data.NotificationService.NtfyPriority == "3" {
|
||||||
|
<option value="3" selected="selected">Default (3)</option>
|
||||||
|
} else {
|
||||||
|
<option value="3">Default (3)</option>
|
||||||
|
}
|
||||||
|
<option value="4">High (4)</option>
|
||||||
|
<option value="5">Urgent (5)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="ntfy_username" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username (Optional)</label>
|
||||||
|
if data.NotificationService.NtfyUsername != "" {
|
||||||
|
<input type="text" id="ntfy_username" name="ntfy_username" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Username for protected topics" value={ data.NotificationService.NtfyUsername }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="ntfy_username" name="ntfy_username" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Username for protected topics" value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="ntfy_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password (Optional)</label>
|
||||||
|
if data.NotificationService.NtfyPassword != "" {
|
||||||
|
<input type="password" id="ntfy_password" name="ntfy_password" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Password for protected topics" value={ data.NotificationService.NtfyPassword }/>
|
||||||
|
} else {
|
||||||
|
<input type="password" id="ntfy_password" name="ntfy_password" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Password for protected topics" value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="ntfy_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Notification Title Template</label>
|
||||||
|
if data.NotificationService.NtfyTitleTemplate != "" {
|
||||||
|
<input type="text" id="ntfy_title_template" name="ntfy_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" value={ data.NotificationService.NtfyTitleTemplate }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="ntfy_title_template" name="ntfy_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="{{job.name}} {{job.status}}" value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="ntfy_message_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
||||||
|
<textarea
|
||||||
|
id="ntfy_message_template"
|
||||||
|
name="ntfy_message_template"
|
||||||
|
rows="4"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
||||||
|
>
|
||||||
|
if data.NotificationService.NtfyMessageTemplate != "" {
|
||||||
|
data.NotificationService.NtfyMessageTemplate
|
||||||
|
} </textarea>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Event Triggers</label>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center">
|
||||||
|
if data.NotificationService.EventTriggers != nil {
|
||||||
|
<input id="ntfy_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_start")) }/>
|
||||||
|
} else {
|
||||||
|
<input id="ntfy_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||||
|
}
|
||||||
|
<label for="ntfy_trigger_job_start" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Start</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
if data.NotificationService.EventTriggers != nil {
|
||||||
|
<input id="ntfy_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_complete")) }/>
|
||||||
|
} else {
|
||||||
|
<input id="ntfy_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||||
|
}
|
||||||
|
<label for="ntfy_trigger_job_complete" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Complete</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
if data.NotificationService.EventTriggers != nil {
|
||||||
|
<input id="ntfy_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_error")) }/>
|
||||||
|
} else {
|
||||||
|
<input id="ntfy_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||||
|
}
|
||||||
|
<label for="ntfy_trigger_job_error" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Error</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Test notification button for Ntfy -->
|
||||||
|
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="test-ntfy-btn"
|
||||||
|
hx-post="/admin/settings/notifications/test"
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-target="#test-notification-result"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-paper-plane mr-1"></i>
|
||||||
|
Send Test Notification
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Send a test notification to verify your Ntfy configuration works correctly before saving.
|
||||||
|
</p>
|
||||||
|
<div id="test-notification-result" class="mt-3 hidden">
|
||||||
|
<!-- Result will be shown here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gotify Fields -->
|
||||||
|
<div id="gotify_fields" class="hidden notification-fields">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="gotify_url" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Gotify Server URL</label>
|
||||||
|
if data.NotificationService.GotifyURL != "" {
|
||||||
|
<input type="url" id="gotify_url" name="gotify_url" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://gotify.example.com" value={ data.NotificationService.GotifyURL }/>
|
||||||
|
} else {
|
||||||
|
<input type="url" id="gotify_url" name="gotify_url" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://gotify.example.com" value=""/>
|
||||||
|
}
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">URL of your Gotify server</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="gotify_token" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Application Token</label>
|
||||||
|
if data.NotificationService.GotifyToken != "" {
|
||||||
|
<input type="text" id="gotify_token" name="gotify_token" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="A-M-XiEQj.zX5d" value={ data.NotificationService.GotifyToken }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="gotify_token" name="gotify_token" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="A-M-XiEQj.zX5d" value=""/>
|
||||||
|
}
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Find this in your Gotify application settings</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="gotify_priority" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Default Priority</label>
|
||||||
|
<select id="gotify_priority" name="gotify_priority" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option value="0">Low (0)</option>
|
||||||
|
if data.NotificationService.GotifyPriority != "" && data.NotificationService.GotifyPriority == "5" {
|
||||||
|
<option value="5" selected="selected">Normal (5)</option>
|
||||||
|
} else {
|
||||||
|
<option value="5">Normal (5)</option>
|
||||||
|
}
|
||||||
|
<option value="8">High (8)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="gotify_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Title Template</label>
|
||||||
|
if data.NotificationService.GotifyTitleTemplate != "" {
|
||||||
|
<input type="text" id="gotify_title_template" name="gotify_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" value={ data.NotificationService.GotifyTitleTemplate }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="gotify_title_template" name="gotify_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="{{job.name}} {{job.status}}" value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="gotify_message_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
||||||
|
<textarea
|
||||||
|
id="gotify_message_template"
|
||||||
|
name="gotify_message_template"
|
||||||
|
rows="4"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
||||||
|
>
|
||||||
|
if data.NotificationService.GotifyMessageTemplate != "" {
|
||||||
|
data.NotificationService.GotifyMessageTemplate
|
||||||
|
}</textarea>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Event Triggers</label>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center">
|
||||||
|
if data.NotificationService.EventTriggers != nil {
|
||||||
|
<input id="gotify_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_start")) }/>
|
||||||
|
} else {
|
||||||
|
<input id="gotify_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||||
|
}
|
||||||
|
<label for="gotify_trigger_job_start" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Start</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
if data.NotificationService.EventTriggers != nil {
|
||||||
|
<input id="gotify_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_complete")) }/>
|
||||||
|
} else {
|
||||||
|
<input id="gotify_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||||
|
}
|
||||||
|
<label for="gotify_trigger_job_complete" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Complete</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
if data.NotificationService.EventTriggers != nil {
|
||||||
|
<input id="gotify_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_error")) }/>
|
||||||
|
} else {
|
||||||
|
<input id="gotify_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||||
|
}
|
||||||
|
<label for="gotify_trigger_job_error" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Error</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Test notification button for Gotify -->
|
||||||
|
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="test-gotify-btn"
|
||||||
|
hx-post="/admin/settings/notifications/test"
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-target="#test-notification-result"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-paper-plane mr-1"></i>
|
||||||
|
Send Test Notification
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Send a test notification to verify your Gotify configuration works correctly before saving.
|
||||||
|
</p>
|
||||||
|
<div id="test-notification-result" class="mt-3 hidden">
|
||||||
|
<!-- Result will be shown here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pushover Fields -->
|
||||||
|
<div id="pushover_fields" class="hidden notification-fields">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushover_app_token" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">API Token/Key</label>
|
||||||
|
if data.NotificationService.PushoverAPIToken != "" {
|
||||||
|
<input type="text" id="pushover_app_token" name="pushover_app_token" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="azGDORePK8gMaC0QOYAMyEEuzJnyUi" value={ data.NotificationService.PushoverAPIToken }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="pushover_app_token" name="pushover_app_token" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="azGDORePK8gMaC0QOYAMyEEuzJnyUi" value=""/>
|
||||||
|
}
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Your application's API token/key from <a href="https://pushover.net/apps" target="_blank" class="text-blue-500 hover:underline">Pushover Dashboard</a></p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushover_user_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">User Key</label>
|
||||||
|
if data.NotificationService.PushoverUserKey != "" {
|
||||||
|
<input type="text" id="pushover_user_key" name="pushover_user_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="uQiRzpo4DXghDmr9QzzfQu27cmVRsG" value={ data.NotificationService.PushoverUserKey }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="pushover_user_key" name="pushover_user_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="uQiRzpo4DXghDmr9QzzfQu27cmVRsG" value=""/>
|
||||||
|
}
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Your user key from <a href="https://pushover.net/" target="_blank" class="text-blue-500 hover:underline">Pushover Dashboard</a></p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushover_device" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Device Name (Optional)</label>
|
||||||
|
if data.NotificationService.PushoverDevice != "" {
|
||||||
|
<input type="text" id="pushover_device" name="pushover_device" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Leave empty to send to all devices" value={ data.NotificationService.PushoverDevice }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="pushover_device" name="pushover_device" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Leave empty to send to all devices" value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushover_priority" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Default Priority</label>
|
||||||
|
<select id="pushover_priority" name="pushover_priority" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option value="-2">Lowest (-2)</option>
|
||||||
|
<option value="-1">Low (-1)</option>
|
||||||
|
if data.NotificationService.PushoverPriority != "" && data.NotificationService.PushoverPriority == "0" {
|
||||||
|
<option value="0" selected="selected">Normal (0)</option>
|
||||||
|
} else {
|
||||||
|
<option value="0">Normal (0)</option>
|
||||||
|
}
|
||||||
|
<option value="1">High (1)</option>
|
||||||
|
<option value="2">Emergency (2)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushover_sound" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Sound</label>
|
||||||
|
<select id="pushover_sound" name="pushover_sound" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option value="pushover">Pushover (default)</option>
|
||||||
|
<option value="bike">Bike</option>
|
||||||
|
<option value="bugle">Bugle</option>
|
||||||
|
<option value="cashregister">Cash Register</option>
|
||||||
|
<option value="classical">Classical</option>
|
||||||
|
<option value="cosmic">Cosmic</option>
|
||||||
|
<option value="falling">Falling</option>
|
||||||
|
<option value="gamelan">Gamelan</option>
|
||||||
|
<option value="incoming">Incoming</option>
|
||||||
|
<option value="intermission">Intermission</option>
|
||||||
|
<option value="magic">Magic</option>
|
||||||
|
<option value="mechanical">Mechanical</option>
|
||||||
|
<option value="pianobar">Piano Bar</option>
|
||||||
|
<option value="siren">Siren</option>
|
||||||
|
<option value="spacealarm">Space Alarm</option>
|
||||||
|
<option value="tugboat">Tug Boat</option>
|
||||||
|
<option value="alien">Alien Alarm (long)</option>
|
||||||
|
<option value="climb">Climb (long)</option>
|
||||||
|
<option value="persistent">Persistent (long)</option>
|
||||||
|
<option value="echo">Echo (long)</option>
|
||||||
|
<option value="updown">Up Down (long)</option>
|
||||||
|
<option value="vibrate">Vibrate Only</option>
|
||||||
|
<option value="none">None (silent)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushover_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Title Template</label>
|
||||||
|
if data.NotificationService.PushoverTitleTemplate != "" {
|
||||||
|
<input type="text" id="pushover_title_template" name="pushover_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" value={ data.NotificationService.PushoverTitleTemplate }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="pushover_title_template" name="pushover_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="'{{job.name}}' {{job.status}}" value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushover_message_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
||||||
|
<textarea
|
||||||
|
id="pushover_message_template"
|
||||||
|
name="pushover_message_template"
|
||||||
|
rows="4"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
||||||
|
>
|
||||||
|
if data.NotificationService.PushoverMessageTemplate != "" {
|
||||||
|
data.NotificationService.PushoverMessageTemplate
|
||||||
|
} </textarea>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Event Triggers</label>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center">
|
||||||
|
if data.NotificationService.EventTriggers != nil {
|
||||||
|
<input id="pushover_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_start")) }/>
|
||||||
|
} else {
|
||||||
|
<input id="pushover_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||||
|
}
|
||||||
|
<label for="pushover_trigger_job_start" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Start</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
if data.NotificationService.EventTriggers != nil {
|
||||||
|
<input id="pushover_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_complete")) }/>
|
||||||
|
} else {
|
||||||
|
<input id="pushover_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||||
|
}
|
||||||
|
<label for="pushover_trigger_job_complete" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Complete</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
if data.NotificationService.EventTriggers != nil {
|
||||||
|
<input id="pushover_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_error")) }/>
|
||||||
|
} else {
|
||||||
|
<input id="pushover_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||||
|
}
|
||||||
|
<label for="pushover_trigger_job_error" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Error</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Test notification button for Pushover -->
|
||||||
|
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="test-pushover-btn"
|
||||||
|
hx-post="/admin/settings/notifications/test"
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-target="#test-notification-result"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-paper-plane mr-1"></i>
|
||||||
|
Send Test Notification
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Send a test notification to verify your Pushover configuration works correctly before saving.
|
||||||
|
</p>
|
||||||
|
<div id="test-notification-result" class="mt-3 hidden">
|
||||||
|
<!-- Result will be shown here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start mb-6 hidden common-fields">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
if data.NotificationService.IsEnabled != false {
|
||||||
|
<input id="is_enabled" name="is_enabled" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(data.NotificationService.IsEnabled) }/>
|
||||||
|
} else {
|
||||||
|
<input id="is_enabled" name="is_enabled" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked="true"/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 text-sm">
|
||||||
|
<label for="is_enabled" class="font-medium text-gray-900 dark:text-white">Enable this notification service</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hidden common-fields">
|
||||||
|
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
|
if data.IsNew {
|
||||||
|
Add Service
|
||||||
|
} else {
|
||||||
|
Save Changes
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Help Notice -->
|
||||||
|
<div class="mt-8 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<i class="fas fa-info-circle text-blue-400 dark:text-blue-400"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<p class="text-sm text-blue-700 dark:text-blue-400">
|
||||||
|
Configure your notification service to receive alerts for job events. Different notification types have different configuration options.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Set dark background color if in dark mode
|
||||||
|
if (document.documentElement.classList.contains('dark')) {
|
||||||
|
document.getElementById('notification-form-container').style.backgroundColor = '#111827';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener for theme changes
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener('click', function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
document.getElementById('notification-form-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,483 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Dialog component for confirmation dialogs using Flowbite modal
|
||||||
|
templ NotificationDialog(id string, title string, message string, confirmClass string, confirmText string, action string, serviceID uint, serviceName string) {
|
||||||
|
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
|
||||||
|
<!-- Modal content -->
|
||||||
|
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
|
||||||
|
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||||
|
<div class="p-6 text-center">
|
||||||
|
if action == "delete" {
|
||||||
|
<i class="fas fa-trash-alt text-red-400 text-3xl mb-4"></i>
|
||||||
|
} else {
|
||||||
|
<i class="fas fa-exclamation-triangle text-yellow-400 text-3xl mb-4"></i>
|
||||||
|
}
|
||||||
|
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{ message }</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={ confirmClass }
|
||||||
|
hx-delete={ fmt.Sprintf("/admin/settings/notifications/%d", serviceID) }
|
||||||
|
hx-target="body"
|
||||||
|
data-service-name={ serviceName }
|
||||||
|
data-service-id={ fmt.Sprint(serviceID) }
|
||||||
|
id={ fmt.Sprintf("delete-btn-%d", serviceID) }
|
||||||
|
onclick={ triggerServiceDelete(id, serviceID, serviceName) }>
|
||||||
|
{ confirmText }
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick={ closeModal(id) } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
script triggerServiceDelete(dialogId string, serviceID uint, serviceName string) {
|
||||||
|
// Hide the dialog
|
||||||
|
document.getElementById(dialogId).classList.add("hidden");
|
||||||
|
document.getElementById(dialogId).classList.remove("flex");
|
||||||
|
|
||||||
|
// Add debugging info
|
||||||
|
console.log(`Notification service deletion triggered for: ${serviceName} (ID: ${serviceID})`);
|
||||||
|
|
||||||
|
// Store data in a way that's accessible to event handlers
|
||||||
|
window.lastDeletedService = {
|
||||||
|
id: serviceID,
|
||||||
|
name: serviceName
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add custom marker to track this deletion
|
||||||
|
window.currentlyDeletingService = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Notifications(ctx context.Context, data SettingsNotificationsData) {
|
||||||
|
@LayoutWithContext("Notification Services", ctx) {
|
||||||
|
<!-- Status and Error Messages -->
|
||||||
|
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Notification system
|
||||||
|
function showToast(message, type) {
|
||||||
|
const toastContainer = document.getElementById('toast-container');
|
||||||
|
|
||||||
|
// Create toast element
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.id = 'toast-' + type + '-' + Date.now();
|
||||||
|
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
||||||
|
toast.role = 'alert';
|
||||||
|
|
||||||
|
// Set toast content based on type
|
||||||
|
let iconClass, bgColorClass, textColorClass;
|
||||||
|
|
||||||
|
if (type === 'success') {
|
||||||
|
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
||||||
|
bgColorClass = 'text-green-500 dark:text-green-200';
|
||||||
|
textColorClass = 'text-green-500 dark:text-green-200';
|
||||||
|
} else if (type === 'error') {
|
||||||
|
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
||||||
|
bgColorClass = 'text-red-500 dark:text-red-200';
|
||||||
|
textColorClass = 'text-red-500 dark:text-red-200';
|
||||||
|
} else {
|
||||||
|
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
||||||
|
bgColorClass = 'text-blue-500 dark:text-blue-200';
|
||||||
|
textColorClass = 'text-blue-500 dark:text-blue-200';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set inner HTML with appropriate icon and message
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
||||||
|
${type === 'success'
|
||||||
|
? '<i class="fas fa-check"></i>'
|
||||||
|
: type === 'error'
|
||||||
|
? '<i class="fas fa-exclamation-circle"></i>'
|
||||||
|
: '<i class="fas fa-info-circle"></i>'}
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 text-sm font-normal">${message}</div>
|
||||||
|
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add toast to container
|
||||||
|
toastContainer.appendChild(toast);
|
||||||
|
|
||||||
|
// Trigger animation after a small delay to ensure the DOM has updated
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('translate-y-16', 'opacity-0');
|
||||||
|
toast.classList.add('translate-y-0', 'opacity-100');
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// Add event listener to close button
|
||||||
|
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
||||||
|
closeButton.addEventListener('click', function() {
|
||||||
|
// Animate out before removing
|
||||||
|
toast.classList.add('opacity-0', 'translate-y-4');
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-remove toast after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.add('opacity-0', 'translate-y-4');
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track all HTMX events for debugging
|
||||||
|
document.addEventListener('htmx:beforeRequest', function(event) {
|
||||||
|
// Check if this is a DELETE request for a notification service
|
||||||
|
const path = event.detail.path;
|
||||||
|
const method = event.detail.verb;
|
||||||
|
|
||||||
|
console.log(`Request path: ${path}, method: ${method}`);
|
||||||
|
|
||||||
|
// Pattern match for notification service deletions (e.g., /admin/settings/notifications/123)
|
||||||
|
if (path && method === 'DELETE' && path.match(/^\/admin\/settings\/notifications\/\d+$/)) {
|
||||||
|
console.log("Detected notification service deletion request via URL pattern");
|
||||||
|
|
||||||
|
// This is definitely a delete request - store this information
|
||||||
|
window.isServiceDeleteRequest = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('htmx:afterRequest', function(event) {
|
||||||
|
// Check for notification service deletion multiple ways
|
||||||
|
const isDeleteRequest =
|
||||||
|
// Check global flag from the triggerServiceDelete function
|
||||||
|
window.currentlyDeletingService ||
|
||||||
|
// Check flag from beforeRequest handler
|
||||||
|
window.isServiceDeleteRequest ||
|
||||||
|
// Check URL pattern directly from this event
|
||||||
|
(event.detail.pathInfo &&
|
||||||
|
event.detail.pathInfo.requestPath &&
|
||||||
|
event.detail.pathInfo.requestPath.match(/^\/admin\/settings\/notifications\/\d+$/) &&
|
||||||
|
event.detail.verb === 'DELETE');
|
||||||
|
|
||||||
|
console.log(`Is delete request: ${isDeleteRequest}`);
|
||||||
|
|
||||||
|
// If this is a successful delete request, show notification
|
||||||
|
if (isDeleteRequest && event.detail.successful) {
|
||||||
|
console.log("Delete request was successful");
|
||||||
|
|
||||||
|
let serviceName = "Unknown";
|
||||||
|
|
||||||
|
// Try multiple sources for service name
|
||||||
|
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||||
|
serviceName = event.detail.elt.getAttribute('data-service-name') || serviceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serviceName === "Unknown" && window.lastDeletedService) {
|
||||||
|
// Fallback to our stored service info
|
||||||
|
serviceName = window.lastDeletedService.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Showing success notification for deleted service: ${serviceName}`);
|
||||||
|
showToast(`Notification service "${serviceName}" deleted successfully`, 'success');
|
||||||
|
|
||||||
|
// Clear flags
|
||||||
|
window.currentlyDeletingService = false;
|
||||||
|
window.isServiceDeleteRequest = false;
|
||||||
|
window.lastDeletedService = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('htmx:responseError', function(event) {
|
||||||
|
console.log("HTMX response error:", event.detail);
|
||||||
|
|
||||||
|
// Similar logic as success but for errors
|
||||||
|
const isDeleteRequest =
|
||||||
|
window.currentlyDeletingService ||
|
||||||
|
window.isServiceDeleteRequest ||
|
||||||
|
(event.detail.pathInfo &&
|
||||||
|
event.detail.pathInfo.requestPath &&
|
||||||
|
event.detail.pathInfo.requestPath.match(/^\/admin\/settings\/notifications\/\d+$/) &&
|
||||||
|
event.detail.verb === 'DELETE');
|
||||||
|
|
||||||
|
let errorMsg = 'An error occurred';
|
||||||
|
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||||
|
errorMsg = event.detail.xhr.responseText;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDeleteRequest) {
|
||||||
|
console.log("Delete request failed");
|
||||||
|
|
||||||
|
let serviceName = "Unknown";
|
||||||
|
|
||||||
|
// Try multiple sources for service name
|
||||||
|
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||||
|
serviceName = event.detail.elt.getAttribute('data-service-name') || serviceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serviceName === "Unknown" && window.lastDeletedService) {
|
||||||
|
// Fallback to our stored service info
|
||||||
|
serviceName = window.lastDeletedService.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
let errorMsg = `Failed to delete notification service "${serviceName}"`;
|
||||||
|
|
||||||
|
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||||
|
errorMsg = `Error: ${event.detail.xhr.responseText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Showing error notification: ${errorMsg}`);
|
||||||
|
showToast(errorMsg, 'error');
|
||||||
|
|
||||||
|
// Clear flags
|
||||||
|
window.currentlyDeletingService = false;
|
||||||
|
window.isServiceDeleteRequest = false;
|
||||||
|
window.lastDeletedService = null;
|
||||||
|
} else {
|
||||||
|
showToast(errorMsg, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle modal hide buttons
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const hideButtons = document.querySelectorAll('[data-modal-hide]');
|
||||||
|
hideButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const modalId = this.getAttribute('data-modal-hide');
|
||||||
|
const modal = document.getElementById(modalId);
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show any success or error messages as toasts
|
||||||
|
if (document.querySelector('.success-message')) {
|
||||||
|
const successMsg = document.querySelector('.success-message').textContent.trim();
|
||||||
|
if (successMsg) {
|
||||||
|
showToast(successMsg, 'success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.querySelector('.error-message')) {
|
||||||
|
const errorMsg = document.querySelector('.error-message').textContent.trim();
|
||||||
|
if (errorMsg) {
|
||||||
|
showToast(errorMsg, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="notifications-container" style="min-height: 100vh; background-color: rgb(249, 250, 251);" class="notifications-page bg-gray-50 dark:bg-gray-900">
|
||||||
|
<div class="pb-8 w-full">
|
||||||
|
<!-- Success Message (hidden, used for HTMX responses) -->
|
||||||
|
if data.SuccessMessage != "" {
|
||||||
|
<div class="hidden success-message">{ data.SuccessMessage }</div>
|
||||||
|
}
|
||||||
|
<!-- Error Message (hidden, used for HTMX responses) -->
|
||||||
|
if data.ErrorMessage != "" {
|
||||||
|
<div class="hidden error-message">{ data.ErrorMessage }</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-bell w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||||
|
Notification Services
|
||||||
|
</h1>
|
||||||
|
<a href="/admin/settings/notifications/new" class="flex items-center justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||||
|
Add Notification Service
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- List of Notification Services -->
|
||||||
|
if len(data.NotificationServices) == 0 {
|
||||||
|
<div class="text-center py-8 bg-white dark:bg-gray-800 shadow-md rounded-lg">
|
||||||
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100 dark:bg-blue-900 mb-4">
|
||||||
|
<i class="fas fa-bell text-2xl text-blue-600 dark:text-blue-400"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">No notification services configured</h3>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 mb-4">Add a notification service to receive alerts for job events.</p>
|
||||||
|
<a href="/admin/settings/notifications/new" class="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||||
|
Add First Notification Service
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 overflow-hidden">
|
||||||
|
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
for _, service := range data.NotificationServices {
|
||||||
|
<li>
|
||||||
|
<div class="block hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||||
|
<div class="px-4 py-4 sm:px-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
if service.Type == "email" {
|
||||||
|
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 dark:bg-blue-900 dark:text-blue-400 mr-3">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
</div>
|
||||||
|
} else if service.Type == "webhook" {
|
||||||
|
<div class="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center text-green-600 dark:bg-green-900 dark:text-green-400 mr-3">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 dark:bg-gray-700 dark:text-gray-400 mr-3">
|
||||||
|
<i class="fas fa-bell"></i>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-blue-600 dark:text-blue-400 truncate">
|
||||||
|
{ service.Name }
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
{ service.Description }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 flex-shrink-0 flex space-x-2">
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL(fmt.Sprintf("/admin/settings/notifications/%d/edit", service.ID)) }
|
||||||
|
class="text-gray-500 bg-white focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 rounded-lg text-sm p-2 mr-1 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<!-- Add notification delete dialog -->
|
||||||
|
@NotificationDialog(
|
||||||
|
fmt.Sprintf("delete-notification-dialog-%d", service.ID),
|
||||||
|
"Delete Notification Service",
|
||||||
|
fmt.Sprintf("Are you sure you want to delete the notification service '%s'? This cannot be undone.", service.Name),
|
||||||
|
"text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800",
|
||||||
|
"Delete",
|
||||||
|
"delete",
|
||||||
|
service.ID,
|
||||||
|
service.Name,
|
||||||
|
)
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={ showModal(fmt.Sprintf("delete-notification-dialog-%d", service.ID)) }
|
||||||
|
class="text-red-500 bg-white focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 rounded-lg text-sm p-2 dark:bg-gray-800 dark:text-red-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 sm:flex sm:justify-between">
|
||||||
|
<div class="sm:flex flex-col md:flex-row gap-2 md:gap-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span
|
||||||
|
class={ "px-2 py-1 text-xs font-medium rounded-full",
|
||||||
|
templ.KV("bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300", service.IsEnabled),
|
||||||
|
templ.KV("bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300", !service.IsEnabled) }
|
||||||
|
>
|
||||||
|
if service.IsEnabled {
|
||||||
|
Active
|
||||||
|
} else {
|
||||||
|
Disabled
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span class="ml-2 px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300 rounded-full">
|
||||||
|
{ service.Type }
|
||||||
|
</span>
|
||||||
|
if len(service.EventTriggers) > 0 && service.Type == "webhook" {
|
||||||
|
<span class="ml-2 px-2 py-1 text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300 rounded-full">
|
||||||
|
{ fmt.Sprintf("%d triggers", len(service.EventTriggers)) }
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
if service.SuccessCount > 0 || service.FailureCount > 0 {
|
||||||
|
<span class="ml-2 px-2 py-1 text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300 rounded-full">
|
||||||
|
{ fmt.Sprintf("%d/%d", service.SuccessCount, service.SuccessCount + service.FailureCount) }
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
if service.Type == "webhook" {
|
||||||
|
<div class="mt-2 md:mt-0 flex items-center space-x-4">
|
||||||
|
<div class="text-xs">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Events:</span>
|
||||||
|
<span class="ml-1 text-gray-900 dark:text-gray-300">
|
||||||
|
if len(service.EventTriggers) == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
for i, trigger := range service.EventTriggers {
|
||||||
|
if i > 0 {
|
||||||
|
<span>, </span>
|
||||||
|
}
|
||||||
|
{ trigger }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Retry:</span>
|
||||||
|
<span class="ml-1 text-gray-900 dark:text-gray-300">
|
||||||
|
if service.RetryPolicy == "" {
|
||||||
|
Default
|
||||||
|
} else {
|
||||||
|
{ service.RetryPolicy }
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="mt-2 md:mt-0 flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="far fa-clock w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||||
|
<p>
|
||||||
|
Last sent:
|
||||||
|
if service.SuccessCount > 0 {
|
||||||
|
"Recently"
|
||||||
|
} else {
|
||||||
|
"Never"
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Help Notice -->
|
||||||
|
<div class="mt-8 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<i class="fas fa-info-circle text-blue-400 dark:text-blue-400"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<p class="text-sm text-blue-700 dark:text-blue-400">
|
||||||
|
Notification services allow the system to send alerts for job events such as completion, errors, or when jobs start.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Set dark background color if in dark mode
|
||||||
|
if (document.documentElement.classList.contains('dark')) {
|
||||||
|
document.getElementById('notifications-container').style.backgroundColor = '#111827';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener for theme changes
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener('click', function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
document.getElementById('notifications-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package dialog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
// "strconv" // No longer needed here
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotificationDialog component for confirmation dialogs using Flowbite modal
|
||||||
|
templ NotificationDialog(id string, title string, message string, confirmClass string, confirmText string, action string, serviceID uint, serviceName string) {
|
||||||
|
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
|
||||||
|
<!-- Modal content -->
|
||||||
|
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
|
||||||
|
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||||
|
<div class="p-6 text-center">
|
||||||
|
if action == "delete" {
|
||||||
|
<i class="fas fa-trash-alt text-red-400 text-3xl mb-4"></i>
|
||||||
|
} else {
|
||||||
|
<i class="fas fa-exclamation-triangle text-yellow-400 text-3xl mb-4"></i>
|
||||||
|
}
|
||||||
|
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{ message }</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={ confirmClass }
|
||||||
|
hx-delete={ fmt.Sprintf("/admin/settings/notifications/%d", serviceID) }
|
||||||
|
hx-target="body"
|
||||||
|
data-service-name={ serviceName }
|
||||||
|
data-service-id={ fmt.Sprint(serviceID) }
|
||||||
|
id={ fmt.Sprintf("delete-btn-%d", serviceID) }
|
||||||
|
onclick={ templ.ComponentScript{Call: fmt.Sprintf("triggerServiceDelete('%s', %d, '%s')", id, serviceID, serviceName)} }>
|
||||||
|
{ confirmText }
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick={ templ.ComponentScript{Call: fmt.Sprintf("closeModal('%s')", id)} } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scripts (triggerServiceDelete, closeModal, showModal) are now expected to be defined globally or in the calling template (e.g., list.templ).
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package dialog
|
||||||
|
|
||||||
|
// DialogScripts provides the JavaScript function specific to the notification delete confirmation dialog.
|
||||||
|
templ DialogScripts() {
|
||||||
|
<script type="text/javascript">
|
||||||
|
// Called when the delete confirmation button is clicked.
|
||||||
|
// Primarily closes the modal; the actual delete is handled by hx-delete.
|
||||||
|
function triggerServiceDelete(dialogId, serviceId, serviceName) {
|
||||||
|
console.log(`Confirmed delete for service: ${serviceName} (ID: ${serviceId}). Closing modal: ${dialogId}`);
|
||||||
|
// Call the global closeModal function defined elsewhere (e.g., app.js)
|
||||||
|
if (typeof closeModal === 'function') {
|
||||||
|
closeModal(dialogId);
|
||||||
|
} else {
|
||||||
|
console.error('Global closeModal function not found.');
|
||||||
|
}
|
||||||
|
// Optional: Show a "Deleting..." toast here if desired.
|
||||||
|
// The hx-delete attribute on the button will trigger the actual backend request.
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.833
|
||||||
|
package dialog
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
// DialogScripts provides the JavaScript function specific to the notification delete confirmation dialog.
|
||||||
|
func DialogScripts() templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<script type=\"text/javascript\">\n\t\t// Called when the delete confirmation button is clicked.\n\t\t// Primarily closes the modal; the actual delete is handled by hx-delete.\n\t\tfunction triggerServiceDelete(dialogId, serviceId, serviceName) {\n\t\t\tconsole.log(`Confirmed delete for service: ${serviceName} (ID: ${serviceId}). Closing modal: ${dialogId}`);\n\t\t\t// Call the global closeModal function defined elsewhere (e.g., app.js)\n\t\t\tif (typeof closeModal === 'function') {\n\t\t\t\tcloseModal(dialogId);\n\t\t\t} else {\n\t\t\t\tconsole.error('Global closeModal function not found.');\n\t\t\t}\n\t\t\t// Optional: Show a \"Deleting...\" toast here if desired.\n\t\t\t// The hx-delete attribute on the button will trigger the actual backend request.\n\t\t}\n\t</script>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.833
|
||||||
|
package dialog
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
// "strconv" // No longer needed here
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotificationDialog component for confirmation dialogs using Flowbite modal
|
||||||
|
func NotificationDialog(id string, title string, message string, confirmClass string, confirmText string, action string, serviceID uint, serviceName string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(id)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 10, Col: 13}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" tabindex=\"-1\" aria-hidden=\"true\" class=\"hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full\"><!-- Backdrop --><div id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s-backdrop", id))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 12, Col: 42}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" class=\"fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm\"></div><!-- Modal content --><div class=\"relative p-4 w-full max-w-md max-h-full mx-auto\"><div class=\"relative bg-white rounded-lg shadow dark:bg-gray-700\"><div class=\"p-6 text-center\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if action == "delete" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<i class=\"fas fa-trash-alt text-red-400 text-3xl mb-4\"></i>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<i class=\"fas fa-exclamation-triangle text-yellow-400 text-3xl mb-4\"></i>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<h3 class=\"mb-5 text-lg font-normal text-gray-500 dark:text-gray-400\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(message)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 22, Col: 84}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</h3>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 = []any{confirmClass}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("triggerServiceDelete('%s', %d, '%s')", id, serviceID, serviceName)})
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<button type=\"button\" class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var5).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" hx-delete=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/admin/settings/notifications/%d", serviceID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 26, Col: 76}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" hx-target=\"body\" data-service-name=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(serviceName)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 28, Col: 37}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" data-service-id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 string
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(serviceID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 29, Col: 45}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 string
|
||||||
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("delete-btn-%d", serviceID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 30, Col: 50}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" onclick=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var11 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("triggerServiceDelete('%s', %d, '%s')", id, serviceID, serviceName)}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11.Call)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var12 string
|
||||||
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(confirmText)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 32, Col: 19}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</button> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("closeModal('%s')", id)})
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<button type=\"button\" onclick=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("closeModal('%s')", id)}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13.Call)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" class=\"text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600\">Cancel</button></div></div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scripts (triggerServiceDelete, closeModal, showModal) are now expected to be defined globally or in the calling template (e.g., list.templ).
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package fields
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||||
|
// No utils needed for this specific template yet
|
||||||
|
)
|
||||||
|
|
||||||
|
templ EmailFields(data types.NotificationFormData) {
|
||||||
|
<!-- TODO: Populate value attributes if editing an email service -->
|
||||||
|
<div id="email_fields" class="hidden notification-fields">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="smtp_host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Host</label>
|
||||||
|
<input type="text" id="smtp_host" name="smtp_host" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="smtp.example.com"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="smtp_port" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Port</label>
|
||||||
|
<input type="number" id="smtp_port" name="smtp_port" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="587"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="smtp_username" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Username</label>
|
||||||
|
<input type="text" id="smtp_username" name="smtp_username" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="user@example.com"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="smtp_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Password</label>
|
||||||
|
<input type="password" id="smtp_password" name="smtp_password" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="from_email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">From Email</label>
|
||||||
|
<input type="email" id="from_email" name="from_email" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="notifications@example.com"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.833
|
||||||
|
package fields
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||||
|
// No utils needed for this specific template yet
|
||||||
|
)
|
||||||
|
|
||||||
|
func EmailFields(data types.NotificationFormData) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- TODO: Populate value attributes if editing an email service --><div id=\"email_fields\" class=\"hidden notification-fields\"><div class=\"mb-6\"><label for=\"smtp_host\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">SMTP Host</label> <input type=\"text\" id=\"smtp_host\" name=\"smtp_host\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"smtp.example.com\"></div><div class=\"mb-6\"><label for=\"smtp_port\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">SMTP Port</label> <input type=\"number\" id=\"smtp_port\" name=\"smtp_port\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"587\"></div><div class=\"mb-6\"><label for=\"smtp_username\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">SMTP Username</label> <input type=\"text\" id=\"smtp_username\" name=\"smtp_username\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"user@example.com\"></div><div class=\"mb-6\"><label for=\"smtp_password\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">SMTP Password</label> <input type=\"password\" id=\"smtp_password\" name=\"smtp_password\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"></div><div class=\"mb-6\"><label for=\"from_email\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">From Email</label> <input type=\"email\" id=\"from_email\" name=\"from_email\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"notifications@example.com\"></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package fields
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||||
|
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||||
|
)
|
||||||
|
|
||||||
|
templ GotifyFields(data types.NotificationFormData) {
|
||||||
|
<div id="gotify_fields" class="hidden notification-fields">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="gotify_url" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Gotify Server URL</label>
|
||||||
|
if data.NotificationService.GotifyURL != "" {
|
||||||
|
<input type="url" id="gotify_url" name="gotify_url" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://gotify.example.com" value={ data.NotificationService.GotifyURL }/>
|
||||||
|
} else {
|
||||||
|
<input type="url" id="gotify_url" name="gotify_url" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://gotify.example.com" value=""/>
|
||||||
|
}
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">URL of your Gotify server</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="gotify_token" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Application Token</label>
|
||||||
|
if data.NotificationService.GotifyToken != "" {
|
||||||
|
<input type="text" id="gotify_token" name="gotify_token" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="A-M-XiEQj.zX5d" value={ data.NotificationService.GotifyToken }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="gotify_token" name="gotify_token" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="A-M-XiEQj.zX5d" value=""/>
|
||||||
|
}
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Find this in your Gotify application settings</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="gotify_priority" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Default Priority</label>
|
||||||
|
<select id="gotify_priority" name="gotify_priority" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option value="0">Low (0)</option>
|
||||||
|
if data.NotificationService.GotifyPriority != "" && data.NotificationService.GotifyPriority == "5" {
|
||||||
|
<option value="5" selected="selected">Normal (5)</option>
|
||||||
|
} else {
|
||||||
|
<option value="5">Normal (5)</option>
|
||||||
|
}
|
||||||
|
<option value="8">High (8)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="gotify_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Title Template</label>
|
||||||
|
if data.NotificationService.GotifyTitleTemplate != "" {
|
||||||
|
<input type="text" id="gotify_title_template" name="gotify_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" value={ data.NotificationService.GotifyTitleTemplate }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="gotify_title_template" name="gotify_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="{{job.name}} {{job.status}}" value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="gotify_message_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
||||||
|
<textarea
|
||||||
|
id="gotify_message_template"
|
||||||
|
name="gotify_message_template"
|
||||||
|
rows="4"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
||||||
|
>
|
||||||
|
if data.NotificationService.GotifyMessageTemplate != "" {
|
||||||
|
data.NotificationService.GotifyMessageTemplate
|
||||||
|
}</textarea>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||||
|
</div>
|
||||||
|
// Removed duplicate Event Triggers section - now handled in form.templ
|
||||||
|
<!-- Test notification button for Gotify -->
|
||||||
|
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="test-gotify-btn"
|
||||||
|
hx-post="/admin/settings/notifications/test"
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-target="#test-notification-result"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-paper-plane mr-1"></i>
|
||||||
|
Send Test Notification
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Send a test notification to verify your Gotify configuration works correctly before saving.
|
||||||
|
</p>
|
||||||
|
<div id="test-notification-result" class="mt-3 hidden">
|
||||||
|
<!-- Result will be shown here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.833
|
||||||
|
package fields
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||||
|
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||||
|
)
|
||||||
|
|
||||||
|
func GotifyFields(data types.NotificationFormData) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"gotify_fields\" class=\"hidden notification-fields\"><div class=\"mb-6\"><label for=\"gotify_url\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Gotify Server URL</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.GotifyURL != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<input type=\"url\" id=\"gotify_url\" name=\"gotify_url\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"https://gotify.example.com\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.GotifyURL)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/gotify.templ`, Line: 13, Col: 407}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<input type=\"url\" id=\"gotify_url\" name=\"gotify_url\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"https://gotify.example.com\" value=\"\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">URL of your Gotify server</p></div><div class=\"mb-6\"><label for=\"gotify_token\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Application Token</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.GotifyToken != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<input type=\"text\" id=\"gotify_token\" name=\"gotify_token\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"A-M-XiEQj.zX5d\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.GotifyToken)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/gotify.templ`, Line: 22, Col: 402}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<input type=\"text\" id=\"gotify_token\" name=\"gotify_token\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"A-M-XiEQj.zX5d\" value=\"\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Find this in your Gotify application settings</p></div><div class=\"mb-6\"><label for=\"gotify_priority\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Default Priority</label> <select id=\"gotify_priority\" name=\"gotify_priority\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"><option value=\"0\">Low (0)</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.GotifyPriority != "" && data.NotificationService.GotifyPriority == "5" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<option value=\"5\" selected=\"selected\">Normal (5)</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<option value=\"5\">Normal (5)</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<option value=\"8\">High (8)</option></select></div><div class=\"mb-6\"><label for=\"gotify_title_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Message Title Template</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.GotifyTitleTemplate != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<input type=\"text\" id=\"gotify_title_template\" name=\"gotify_title_template\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.GotifyTitleTemplate)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/gotify.templ`, Line: 43, Col: 399}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<input type=\"text\" id=\"gotify_title_template\" name=\"gotify_title_template\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"{{job.name}} {{job.status}}\" value=\"\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div><div class=\"mb-6\"><label for=\"gotify_message_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Message Body Template</label> <textarea id=\"gotify_message_template\" name=\"gotify_message_template\" rows=\"4\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes).\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.GotifyMessageTemplate != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "data.NotificationService.GotifyMessageTemplate")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</textarea><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p></div><!-- Test notification button for Gotify --><div class=\"mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600\"><div class=\"flex items-center justify-between mb-2\"><h4 class=\"text-base font-medium text-gray-900 dark:text-white\">Test Configuration</h4><button type=\"button\" id=\"test-gotify-btn\" hx-post=\"/admin/settings/notifications/test\" hx-trigger=\"click\" hx-target=\"#test-notification-result\" hx-swap=\"outerHTML\" class=\"px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-paper-plane mr-1\"></i> Send Test Notification</button></div><p class=\"text-sm text-gray-500 dark:text-gray-400\">Send a test notification to verify your Gotify configuration works correctly before saving.</p><div id=\"test-notification-result\" class=\"mt-3 hidden\"><!-- Result will be shown here --></div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package fields
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||||
|
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||||
|
)
|
||||||
|
|
||||||
|
templ NtfyFields(data types.NotificationFormData) {
|
||||||
|
<div id="ntfy_fields" class="hidden notification-fields">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="ntfy_server" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Ntfy Server</label>
|
||||||
|
if data.NotificationService.NtfyServer != "" {
|
||||||
|
<input type="url" id="ntfy_server" name="ntfy_server" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://ntfy.sh" value={ data.NotificationService.NtfyServer }/>
|
||||||
|
} else {
|
||||||
|
<input type="url" id="ntfy_server" name="ntfy_server" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://ntfy.sh" value="https://ntfy.sh"/>
|
||||||
|
}
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">The Ntfy server URL (default: ntfy.sh)</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="ntfy_topic" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Topic</label>
|
||||||
|
if data.NotificationService.NtfyTopic != "" {
|
||||||
|
<input type="text" id="ntfy_topic" name="ntfy_topic" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="your-unique-topic" value={ data.NotificationService.NtfyTopic }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="ntfy_topic" name="ntfy_topic" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="your-unique-topic" value="gomft"/>
|
||||||
|
}
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Choose a unique, unguessable topic name</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="ntfy_priority" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Default Priority</label>
|
||||||
|
<select id="ntfy_priority" name="ntfy_priority" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option value="1">Low (1)</option>
|
||||||
|
if data.NotificationService.NtfyPriority == "3" {
|
||||||
|
<option value="3" selected="selected">Default (3)</option>
|
||||||
|
} else {
|
||||||
|
<option value="3">Default (3)</option>
|
||||||
|
}
|
||||||
|
<option value="4">High (4)</option>
|
||||||
|
<option value="5">Urgent (5)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="ntfy_username" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username (Optional)</label>
|
||||||
|
if data.NotificationService.NtfyUsername != "" {
|
||||||
|
<input type="text" id="ntfy_username" name="ntfy_username" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Username for protected topics" value={ data.NotificationService.NtfyUsername }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="ntfy_username" name="ntfy_username" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Username for protected topics" value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="ntfy_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password (Optional)</label>
|
||||||
|
if data.NotificationService.NtfyPassword != "" {
|
||||||
|
<input type="password" id="ntfy_password" name="ntfy_password" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Password for protected topics" value={ data.NotificationService.NtfyPassword }/>
|
||||||
|
} else {
|
||||||
|
<input type="password" id="ntfy_password" name="ntfy_password" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Password for protected topics" value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="ntfy_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Notification Title Template</label>
|
||||||
|
if data.NotificationService.NtfyTitleTemplate != "" {
|
||||||
|
<input type="text" id="ntfy_title_template" name="ntfy_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" value={ data.NotificationService.NtfyTitleTemplate }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="ntfy_title_template" name="ntfy_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="{{job.name}} {{job.status}}" value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="ntfy_message_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
||||||
|
<textarea
|
||||||
|
id="ntfy_message_template"
|
||||||
|
name="ntfy_message_template"
|
||||||
|
rows="4"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
||||||
|
>
|
||||||
|
if data.NotificationService.NtfyMessageTemplate != "" {
|
||||||
|
data.NotificationService.NtfyMessageTemplate
|
||||||
|
} </textarea>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||||
|
</div>
|
||||||
|
// Removed duplicate Event Triggers section - now handled in form.templ
|
||||||
|
<!-- Test notification button for Ntfy -->
|
||||||
|
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="test-ntfy-btn"
|
||||||
|
hx-post="/admin/settings/notifications/test"
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-target="#test-notification-result"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-paper-plane mr-1"></i>
|
||||||
|
Send Test Notification
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Send a test notification to verify your Ntfy configuration works correctly before saving.
|
||||||
|
</p>
|
||||||
|
<div id="test-notification-result" class="mt-3 hidden">
|
||||||
|
<!-- Result will be shown here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.833
|
||||||
|
package fields
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||||
|
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||||
|
)
|
||||||
|
|
||||||
|
func NtfyFields(data types.NotificationFormData) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"ntfy_fields\" class=\"hidden notification-fields\"><div class=\"mb-6\"><label for=\"ntfy_server\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Ntfy Server</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.NtfyServer != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<input type=\"url\" id=\"ntfy_server\" name=\"ntfy_server\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"https://ntfy.sh\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.NtfyServer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/ntfy.templ`, Line: 13, Col: 399}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<input type=\"url\" id=\"ntfy_server\" name=\"ntfy_server\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"https://ntfy.sh\" value=\"https://ntfy.sh\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">The Ntfy server URL (default: ntfy.sh)</p></div><div class=\"mb-6\"><label for=\"ntfy_topic\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Topic</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.NtfyTopic != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<input type=\"text\" id=\"ntfy_topic\" name=\"ntfy_topic\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"your-unique-topic\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.NtfyTopic)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/ntfy.templ`, Line: 22, Col: 399}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<input type=\"text\" id=\"ntfy_topic\" name=\"ntfy_topic\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"your-unique-topic\" value=\"gomft\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Choose a unique, unguessable topic name</p></div><div class=\"mb-6\"><label for=\"ntfy_priority\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Default Priority</label> <select id=\"ntfy_priority\" name=\"ntfy_priority\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"><option value=\"1\">Low (1)</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.NtfyPriority == "3" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<option value=\"3\" selected=\"selected\">Default (3)</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<option value=\"3\">Default (3)</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<option value=\"4\">High (4)</option> <option value=\"5\">Urgent (5)</option></select></div><div class=\"mb-6\"><label for=\"ntfy_username\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Username (Optional)</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.NtfyUsername != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<input type=\"text\" id=\"ntfy_username\" name=\"ntfy_username\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Username for protected topics\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.NtfyUsername)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/ntfy.templ`, Line: 44, Col: 420}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<input type=\"text\" id=\"ntfy_username\" name=\"ntfy_username\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Username for protected topics\" value=\"\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div><div class=\"mb-6\"><label for=\"ntfy_password\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Password (Optional)</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.NtfyPassword != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<input type=\"password\" id=\"ntfy_password\" name=\"ntfy_password\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Password for protected topics\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.NtfyPassword)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/ntfy.templ`, Line: 52, Col: 424}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<input type=\"password\" id=\"ntfy_password\" name=\"ntfy_password\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Password for protected topics\" value=\"\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div><div class=\"mb-6\"><label for=\"ntfy_title_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Notification Title Template</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.NtfyTitleTemplate != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<input type=\"text\" id=\"ntfy_title_template\" name=\"ntfy_title_template\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.NtfyTitleTemplate)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/ntfy.templ`, Line: 60, Col: 393}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<input type=\"text\" id=\"ntfy_title_template\" name=\"ntfy_title_template\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"{{job.name}} {{job.status}}\" value=\"\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div><div class=\"mb-6\"><label for=\"ntfy_message_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Message Body Template</label> <textarea id=\"ntfy_message_template\" name=\"ntfy_message_template\" rows=\"4\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes).\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.NtfyMessageTemplate != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "data.NotificationService.NtfyMessageTemplate")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</textarea><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p></div><!-- Test notification button for Ntfy --><div class=\"mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600\"><div class=\"flex items-center justify-between mb-2\"><h4 class=\"text-base font-medium text-gray-900 dark:text-white\">Test Configuration</h4><button type=\"button\" id=\"test-ntfy-btn\" hx-post=\"/admin/settings/notifications/test\" hx-trigger=\"click\" hx-target=\"#test-notification-result\" hx-swap=\"outerHTML\" class=\"px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-paper-plane mr-1\"></i> Send Test Notification</button></div><p class=\"text-sm text-gray-500 dark:text-gray-400\">Send a test notification to verify your Ntfy configuration works correctly before saving.</p><div id=\"test-notification-result\" class=\"mt-3 hidden\"><!-- Result will be shown here --></div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package fields
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||||
|
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||||
|
)
|
||||||
|
|
||||||
|
templ PushbulletFields(data types.NotificationFormData) {
|
||||||
|
<div id="pushbullet_fields" class="hidden notification-fields">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushbullet_api_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">API Key</label>
|
||||||
|
if data.NotificationService.PushbulletAPIKey != "" {
|
||||||
|
<input type="text" id="pushbullet_api_key" name="pushbullet_api_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="o.XyzAbCdEfGhIjKlMnOpQrSt" value={ data.NotificationService.PushbulletAPIKey }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="pushbullet_api_key" name="pushbullet_api_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="o.XyzAbCdEfGhIjKlMnOpQrSt" value=""/>
|
||||||
|
}
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Get your API key from <a href="https://www.pushbullet.com/#settings/account" target="_blank" class="text-blue-500 hover:underline">Pushbullet Account Settings</a></p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushbullet_device_iden" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Device Identifier (Optional)</label>
|
||||||
|
if data.NotificationService.PushbulletDeviceID != "" {
|
||||||
|
<input type="text" id="pushbullet_device_iden" name="pushbullet_device_iden" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Leave empty to send to all devices" value={ data.NotificationService.PushbulletDeviceID }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="pushbullet_device_iden" name="pushbullet_device_iden" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Leave empty to send to all devices" value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushbullet_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Notification Title Template</label>
|
||||||
|
if data.NotificationService.PushbulletTitleTemplate != "" {
|
||||||
|
<input type="text" id="pushbullet_title_template" name="pushbullet_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" value={ data.NotificationService.PushbulletTitleTemplate }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="pushbullet_title_template" name="pushbullet_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="{{job.name}} {{job.status}}" value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushbullet_body_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
||||||
|
<textarea
|
||||||
|
id="pushbullet_body_template"
|
||||||
|
name="pushbullet_body_template"
|
||||||
|
rows="4"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
||||||
|
>
|
||||||
|
if data.NotificationService.PushbulletBodyTemplate != "" {
|
||||||
|
data.NotificationService.PushbulletBodyTemplate
|
||||||
|
} </textarea>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||||
|
</div>
|
||||||
|
// Removed duplicate Event Triggers section - now handled in form.templ
|
||||||
|
<!-- Test notification button for Pushbullet -->
|
||||||
|
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="test-pushbullet-btn"
|
||||||
|
hx-post="/admin/settings/notifications/test"
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-target="#test-notification-result"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-paper-plane mr-1"></i>
|
||||||
|
Send Test Notification
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Send a test notification to verify your Pushbullet configuration works correctly before saving.
|
||||||
|
</p>
|
||||||
|
<div id="test-notification-result" class="mt-3 hidden">
|
||||||
|
<!-- Result will be shown here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.833
|
||||||
|
package fields
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||||
|
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||||
|
)
|
||||||
|
|
||||||
|
func PushbulletFields(data types.NotificationFormData) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"pushbullet_fields\" class=\"hidden notification-fields\"><div class=\"mb-6\"><label for=\"pushbullet_api_key\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">API Key</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.PushbulletAPIKey != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<input type=\"text\" id=\"pushbullet_api_key\" name=\"pushbullet_api_key\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"o.XyzAbCdEfGhIjKlMnOpQrSt\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushbulletAPIKey)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushbullet.templ`, Line: 13, Col: 430}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<input type=\"text\" id=\"pushbullet_api_key\" name=\"pushbullet_api_key\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"o.XyzAbCdEfGhIjKlMnOpQrSt\" value=\"\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Get your API key from <a href=\"https://www.pushbullet.com/#settings/account\" target=\"_blank\" class=\"text-blue-500 hover:underline\">Pushbullet Account Settings</a></p></div><div class=\"mb-6\"><label for=\"pushbullet_device_iden\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Device Identifier (Optional)</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.PushbulletDeviceID != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<input type=\"text\" id=\"pushbullet_device_iden\" name=\"pushbullet_device_iden\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Leave empty to send to all devices\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushbulletDeviceID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushbullet.templ`, Line: 22, Col: 449}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<input type=\"text\" id=\"pushbullet_device_iden\" name=\"pushbullet_device_iden\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Leave empty to send to all devices\" value=\"\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div><div class=\"mb-6\"><label for=\"pushbullet_title_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Notification Title Template</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.PushbulletTitleTemplate != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<input type=\"text\" id=\"pushbullet_title_template\" name=\"pushbullet_title_template\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushbulletTitleTemplate)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushbullet.templ`, Line: 30, Col: 411}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<input type=\"text\" id=\"pushbullet_title_template\" name=\"pushbullet_title_template\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"{{job.name}} {{job.status}}\" value=\"\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div><div class=\"mb-6\"><label for=\"pushbullet_body_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Message Body Template</label> <textarea id=\"pushbullet_body_template\" name=\"pushbullet_body_template\" rows=\"4\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes).\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.PushbulletBodyTemplate != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "data.NotificationService.PushbulletBodyTemplate")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</textarea><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p></div><!-- Test notification button for Pushbullet --><div class=\"mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600\"><div class=\"flex items-center justify-between mb-2\"><h4 class=\"text-base font-medium text-gray-900 dark:text-white\">Test Configuration</h4><button type=\"button\" id=\"test-pushbullet-btn\" hx-post=\"/admin/settings/notifications/test\" hx-trigger=\"click\" hx-target=\"#test-notification-result\" hx-swap=\"outerHTML\" class=\"px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-paper-plane mr-1\"></i> Send Test Notification</button></div><p class=\"text-sm text-gray-500 dark:text-gray-400\">Send a test notification to verify your Pushbullet configuration works correctly before saving.</p><div id=\"test-notification-result\" class=\"mt-3 hidden\"><!-- Result will be shown here --></div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package fields
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||||
|
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||||
|
)
|
||||||
|
|
||||||
|
templ PushoverFields(data types.NotificationFormData) {
|
||||||
|
<div id="pushover_fields" class="hidden notification-fields">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushover_app_token" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">API Token/Key</label>
|
||||||
|
if data.NotificationService.PushoverAPIToken != "" {
|
||||||
|
<input type="text" id="pushover_app_token" name="pushover_app_token" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="azGDORePK8gMaC0QOYAMyEEuzJnyUi" value={ data.NotificationService.PushoverAPIToken }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="pushover_app_token" name="pushover_app_token" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="azGDORePK8gMaC0QOYAMyEEuzJnyUi" value=""/>
|
||||||
|
}
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Your application's API token/key from <a href="https://pushover.net/apps" target="_blank" class="text-blue-500 hover:underline">Pushover Dashboard</a></p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushover_user_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">User Key</label>
|
||||||
|
if data.NotificationService.PushoverUserKey != "" {
|
||||||
|
<input type="text" id="pushover_user_key" name="pushover_user_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="uQiRzpo4DXghDmr9QzzfQu27cmVRsG" value={ data.NotificationService.PushoverUserKey }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="pushover_user_key" name="pushover_user_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="uQiRzpo4DXghDmr9QzzfQu27cmVRsG" value=""/>
|
||||||
|
}
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Your user key from <a href="https://pushover.net/" target="_blank" class="text-blue-500 hover:underline">Pushover Dashboard</a></p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushover_device" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Device Name (Optional)</label>
|
||||||
|
if data.NotificationService.PushoverDevice != "" {
|
||||||
|
<input type="text" id="pushover_device" name="pushover_device" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Leave empty to send to all devices" value={ data.NotificationService.PushoverDevice }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="pushover_device" name="pushover_device" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Leave empty to send to all devices" value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushover_priority" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Default Priority</label>
|
||||||
|
<select id="pushover_priority" name="pushover_priority" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option value="-2">Lowest (-2)</option>
|
||||||
|
<option value="-1">Low (-1)</option>
|
||||||
|
if data.NotificationService.PushoverPriority != "" && data.NotificationService.PushoverPriority == "0" {
|
||||||
|
<option value="0" selected="selected">Normal (0)</option>
|
||||||
|
} else {
|
||||||
|
<option value="0">Normal (0)</option>
|
||||||
|
}
|
||||||
|
<option value="1">High (1)</option>
|
||||||
|
<option value="2">Emergency (2)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushover_sound" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Sound</label>
|
||||||
|
<select id="pushover_sound" name="pushover_sound" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option value="pushover">Pushover (default)</option>
|
||||||
|
<option value="bike">Bike</option>
|
||||||
|
<option value="bugle">Bugle</option>
|
||||||
|
<option value="cashregister">Cash Register</option>
|
||||||
|
<option value="classical">Classical</option>
|
||||||
|
<option value="cosmic">Cosmic</option>
|
||||||
|
<option value="falling">Falling</option>
|
||||||
|
<option value="gamelan">Gamelan</option>
|
||||||
|
<option value="incoming">Incoming</option>
|
||||||
|
<option value="intermission">Intermission</option>
|
||||||
|
<option value="magic">Magic</option>
|
||||||
|
<option value="mechanical">Mechanical</option>
|
||||||
|
<option value="pianobar">Piano Bar</option>
|
||||||
|
<option value="siren">Siren</option>
|
||||||
|
<option value="spacealarm">Space Alarm</option>
|
||||||
|
<option value="tugboat">Tug Boat</option>
|
||||||
|
<option value="alien">Alien Alarm (long)</option>
|
||||||
|
<option value="climb">Climb (long)</option>
|
||||||
|
<option value="persistent">Persistent (long)</option>
|
||||||
|
<option value="echo">Echo (long)</option>
|
||||||
|
<option value="updown">Up Down (long)</option>
|
||||||
|
<option value="vibrate">Vibrate Only</option>
|
||||||
|
<option value="none">None (silent)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushover_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Title Template</label>
|
||||||
|
if data.NotificationService.PushoverTitleTemplate != "" {
|
||||||
|
<input type="text" id="pushover_title_template" name="pushover_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" value={ data.NotificationService.PushoverTitleTemplate }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="pushover_title_template" name="pushover_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="'{{job.name}}' {{job.status}}" value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="pushover_message_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
||||||
|
<textarea
|
||||||
|
id="pushover_message_template"
|
||||||
|
name="pushover_message_template"
|
||||||
|
rows="4"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
||||||
|
>
|
||||||
|
if data.NotificationService.PushoverMessageTemplate != "" {
|
||||||
|
data.NotificationService.PushoverMessageTemplate
|
||||||
|
} </textarea>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||||
|
</div>
|
||||||
|
// Removed duplicate Event Triggers section - now handled in form.templ
|
||||||
|
<!-- Test notification button for Pushover -->
|
||||||
|
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="test-pushover-btn"
|
||||||
|
hx-post="/admin/settings/notifications/test"
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-target="#test-notification-result"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-paper-plane mr-1"></i>
|
||||||
|
Send Test Notification
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Send a test notification to verify your Pushover configuration works correctly before saving.
|
||||||
|
</p>
|
||||||
|
<div id="test-notification-result" class="mt-3 hidden">
|
||||||
|
<!-- Result will be shown here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.833
|
||||||
|
package fields
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||||
|
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||||
|
)
|
||||||
|
|
||||||
|
func PushoverFields(data types.NotificationFormData) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"pushover_fields\" class=\"hidden notification-fields\"><div class=\"mb-6\"><label for=\"pushover_app_token\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">API Token/Key</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.PushoverAPIToken != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<input type=\"text\" id=\"pushover_app_token\" name=\"pushover_app_token\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"azGDORePK8gMaC0QOYAMyEEuzJnyUi\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushoverAPIToken)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushover.templ`, Line: 13, Col: 435}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<input type=\"text\" id=\"pushover_app_token\" name=\"pushover_app_token\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"azGDORePK8gMaC0QOYAMyEEuzJnyUi\" value=\"\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Your application's API token/key from <a href=\"https://pushover.net/apps\" target=\"_blank\" class=\"text-blue-500 hover:underline\">Pushover Dashboard</a></p></div><div class=\"mb-6\"><label for=\"pushover_user_key\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">User Key</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.PushoverUserKey != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<input type=\"text\" id=\"pushover_user_key\" name=\"pushover_user_key\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"uQiRzpo4DXghDmr9QzzfQu27cmVRsG\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushoverUserKey)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushover.templ`, Line: 22, Col: 432}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<input type=\"text\" id=\"pushover_user_key\" name=\"pushover_user_key\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"uQiRzpo4DXghDmr9QzzfQu27cmVRsG\" value=\"\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Your user key from <a href=\"https://pushover.net/\" target=\"_blank\" class=\"text-blue-500 hover:underline\">Pushover Dashboard</a></p></div><div class=\"mb-6\"><label for=\"pushover_device\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Device Name (Optional)</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.PushoverDevice != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<input type=\"text\" id=\"pushover_device\" name=\"pushover_device\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Leave empty to send to all devices\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushoverDevice)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushover.templ`, Line: 31, Col: 431}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<input type=\"text\" id=\"pushover_device\" name=\"pushover_device\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Leave empty to send to all devices\" value=\"\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div><div class=\"mb-6\"><label for=\"pushover_priority\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Default Priority</label> <select id=\"pushover_priority\" name=\"pushover_priority\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"><option value=\"-2\">Lowest (-2)</option> <option value=\"-1\">Low (-1)</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.PushoverPriority != "" && data.NotificationService.PushoverPriority == "0" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<option value=\"0\" selected=\"selected\">Normal (0)</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<option value=\"0\">Normal (0)</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<option value=\"1\">High (1)</option> <option value=\"2\">Emergency (2)</option></select></div><div class=\"mb-6\"><label for=\"pushover_sound\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Sound</label> <select id=\"pushover_sound\" name=\"pushover_sound\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"><option value=\"pushover\">Pushover (default)</option> <option value=\"bike\">Bike</option> <option value=\"bugle\">Bugle</option> <option value=\"cashregister\">Cash Register</option> <option value=\"classical\">Classical</option> <option value=\"cosmic\">Cosmic</option> <option value=\"falling\">Falling</option> <option value=\"gamelan\">Gamelan</option> <option value=\"incoming\">Incoming</option> <option value=\"intermission\">Intermission</option> <option value=\"magic\">Magic</option> <option value=\"mechanical\">Mechanical</option> <option value=\"pianobar\">Piano Bar</option> <option value=\"siren\">Siren</option> <option value=\"spacealarm\">Space Alarm</option> <option value=\"tugboat\">Tug Boat</option> <option value=\"alien\">Alien Alarm (long)</option> <option value=\"climb\">Climb (long)</option> <option value=\"persistent\">Persistent (long)</option> <option value=\"echo\">Echo (long)</option> <option value=\"updown\">Up Down (long)</option> <option value=\"vibrate\">Vibrate Only</option> <option value=\"none\">None (silent)</option></select></div><div class=\"mb-6\"><label for=\"pushover_title_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Message Title Template</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.PushoverTitleTemplate != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<input type=\"text\" id=\"pushover_title_template\" name=\"pushover_title_template\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushoverTitleTemplate)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushover.templ`, Line: 81, Col: 405}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<input type=\"text\" id=\"pushover_title_template\" name=\"pushover_title_template\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"'{{job.name}}' {{job.status}}\" value=\"\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div><div class=\"mb-6\"><label for=\"pushover_message_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Message Body Template</label> <textarea id=\"pushover_message_template\" name=\"pushover_message_template\" rows=\"4\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes).\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.PushoverMessageTemplate != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "data.NotificationService.PushoverMessageTemplate")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</textarea><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p></div><!-- Test notification button for Pushover --><div class=\"mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600\"><div class=\"flex items-center justify-between mb-2\"><h4 class=\"text-base font-medium text-gray-900 dark:text-white\">Test Configuration</h4><button type=\"button\" id=\"test-pushover-btn\" hx-post=\"/admin/settings/notifications/test\" hx-trigger=\"click\" hx-target=\"#test-notification-result\" hx-swap=\"outerHTML\" class=\"px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-paper-plane mr-1\"></i> Send Test Notification</button></div><p class=\"text-sm text-gray-500 dark:text-gray-400\">Send a test notification to verify your Pushover configuration works correctly before saving.</p><div id=\"test-notification-result\" class=\"mt-3 hidden\"><!-- Result will be shown here --></div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package fields
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||||
|
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||||
|
)
|
||||||
|
|
||||||
|
templ WebhookFields(data types.NotificationFormData) {
|
||||||
|
<div id="webhook_fields" class="hidden notification-fields">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="webhook_url" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Webhook URL</label>
|
||||||
|
if data.NotificationService.WebhookURL != "" {
|
||||||
|
<input type="url" id="webhook_url" name="webhook_url" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://api.example.com/webhook" value={ data.NotificationService.WebhookURL }/>
|
||||||
|
} else {
|
||||||
|
<input type="url" id="webhook_url" name="webhook_url" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://api.example.com/webhook" value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="method" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">HTTP Method</label>
|
||||||
|
<select id="method" name="method" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
if data.NotificationService.Method != "" {
|
||||||
|
if data.NotificationService.Method == "POST" {
|
||||||
|
<option value="POST" selected="selected">POST</option>
|
||||||
|
} else {
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
}
|
||||||
|
if data.NotificationService.Method == "PUT" {
|
||||||
|
<option value="PUT" selected="selected">PUT</option>
|
||||||
|
} else {
|
||||||
|
<option value="PUT">PUT</option>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
<option value="PUT">PUT</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="headers" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Headers (JSON)</label>
|
||||||
|
if data.NotificationService.Headers != "" {
|
||||||
|
<textarea id="headers" name="headers" rows="3" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder='{"Content-Type": "application/json", "Authorization": "Bearer token"}'>{ data.NotificationService.Headers }</textarea>
|
||||||
|
} else {
|
||||||
|
<textarea id="headers" name="headers" rows="3" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder='{"Content-Type": "application/json", "Authorization": "Bearer token"}'></textarea>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="payload_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Payload Template (JSON)</label>
|
||||||
|
<textarea
|
||||||
|
id="payload_template"
|
||||||
|
name="payload_template"
|
||||||
|
rows="5"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder='{
|
||||||
|
"event": "{{job.event}}",
|
||||||
|
"job": {
|
||||||
|
"id": "{{job.id}}",
|
||||||
|
"name": "{{job.name}}",
|
||||||
|
"status": "{{job.status}}",
|
||||||
|
"message": "{{job.message}}",
|
||||||
|
"started_at": "{{job.started_at}}",
|
||||||
|
"completed_at": "{{job.completed_at}}",
|
||||||
|
"duration_seconds": {{job.duration_seconds}},
|
||||||
|
"config_id": "{{job.config_id}}",
|
||||||
|
"config_name": "{{job.config_name}}",
|
||||||
|
"transfer_bytes": {{job.transfer_bytes}},
|
||||||
|
"file_count": {{job.file_count}}
|
||||||
|
},
|
||||||
|
"instance": {
|
||||||
|
"id": "{{instance.id}}",
|
||||||
|
"name": "{{instance.name}}",
|
||||||
|
"version": "{{instance.version}}",
|
||||||
|
"environment": "{{instance.environment}}"
|
||||||
|
},
|
||||||
|
"timestamp": "{{timestamp}}",
|
||||||
|
"notification_id": "{{notification.id}}"
|
||||||
|
}'
|
||||||
|
>
|
||||||
|
if data.NotificationService.PayloadTemplate != "" {
|
||||||
|
data.NotificationService.PayloadTemplate
|
||||||
|
} </textarea>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||||
|
</div>
|
||||||
|
// Removed duplicate Event Triggers section - now handled in form.templ
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="secret_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Key (for signature verification)</label>
|
||||||
|
if data.NotificationService.SecretKey != "" {
|
||||||
|
<input type="text" id="secret_key" name="secret_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Optional signature verification key" value={ data.NotificationService.SecretKey }/>
|
||||||
|
}
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">If provided, all webhooks will include an X-GoMFT-Signature header</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="retry_policy" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Retry Policy</label>
|
||||||
|
<select id="retry_policy" name="retry_policy" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
if data.NotificationService.RetryPolicy != "" {
|
||||||
|
if data.NotificationService.RetryPolicy == "none" {
|
||||||
|
<option value="none" selected="selected">No retries</option>
|
||||||
|
} else {
|
||||||
|
<option value="none">No retries</option>
|
||||||
|
}
|
||||||
|
if data.NotificationService.RetryPolicy == "simple" {
|
||||||
|
<option value="simple" selected="selected">Simple (3 retries)</option>
|
||||||
|
} else {
|
||||||
|
<option value="simple">Simple (3 retries)</option>
|
||||||
|
}
|
||||||
|
if data.NotificationService.RetryPolicy == "exponential" {
|
||||||
|
<option value="exponential" selected="selected">Exponential backoff</option>
|
||||||
|
} else {
|
||||||
|
<option value="exponential">Exponential backoff</option>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
<option value="none">No retries</option>
|
||||||
|
<option value="simple">Simple (3 retries)</option>
|
||||||
|
<option value="exponential">Exponential backoff</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<!-- Test notification button -->
|
||||||
|
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="test-webhook-btn"
|
||||||
|
hx-post="/admin/settings/notifications/test"
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-target="#test-notification-result"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-paper-plane mr-1"></i>
|
||||||
|
Send Test Notification
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Send a test notification to verify your configuration works correctly before saving.
|
||||||
|
</p>
|
||||||
|
<div id="test-notification-result" class="mt-3 hidden">
|
||||||
|
<!-- Result will be shown here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.833
|
||||||
|
package fields
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||||
|
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||||
|
)
|
||||||
|
|
||||||
|
func WebhookFields(data types.NotificationFormData) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"webhook_fields\" class=\"hidden notification-fields\"><div class=\"mb-6\"><label for=\"webhook_url\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Webhook URL</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.WebhookURL != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<input type=\"url\" id=\"webhook_url\" name=\"webhook_url\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"https://api.example.com/webhook\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.WebhookURL)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/webhook.templ`, Line: 13, Col: 415}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<input type=\"url\" id=\"webhook_url\" name=\"webhook_url\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"https://api.example.com/webhook\" value=\"\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><div class=\"mb-6\"><label for=\"method\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">HTTP Method</label> <select id=\"method\" name=\"method\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.Method != "" {
|
||||||
|
if data.NotificationService.Method == "POST" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<option value=\"POST\" selected=\"selected\">POST</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<option value=\"POST\">POST</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.Method == "PUT" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<option value=\"PUT\" selected=\"selected\">PUT</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<option value=\"PUT\">PUT</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<option value=\"POST\">POST</option> <option value=\"PUT\">PUT</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</select></div><div class=\"mb-6\"><label for=\"headers\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Headers (JSON)</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.Headers != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<textarea id=\"headers\" name=\"headers\" rows=\"3\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"{"Content-Type": "application/json", "Authorization": "Bearer token"}\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.Headers)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/webhook.templ`, Line: 41, Col: 437}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</textarea>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<textarea id=\"headers\" name=\"headers\" rows=\"3\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"{"Content-Type": "application/json", "Authorization": "Bearer token"}\"></textarea>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div><div class=\"mb-6\"><label for=\"payload_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Payload Template (JSON)</label> <textarea id=\"payload_template\" name=\"payload_template\" rows=\"5\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"{\n\t"event": "{{job.event}}",\n\t"job": {\n\t\t\t"id": "{{job.id}}",\n\t\t\t"name": "{{job.name}}",\n\t\t\t"status": "{{job.status}}",\n\t\t\t"message": "{{job.message}}",\n\t\t\t"started_at": "{{job.started_at}}",\n\t\t\t"completed_at": "{{job.completed_at}}",\n\t\t\t"duration_seconds": {{job.duration_seconds}},\n\t\t\t"config_id": "{{job.config_id}}",\n\t\t\t"config_name": "{{job.config_name}}",\n\t\t\t"transfer_bytes": {{job.transfer_bytes}},\n\t\t\t"file_count": {{job.file_count}}\n\t},\n\t"instance": {\n\t\t\t"id": "{{instance.id}}",\n\t\t\t"name": "{{instance.name}}",\n\t\t\t"version": "{{instance.version}}",\n\t\t\t"environment": "{{instance.environment}}"\n\t},\n\t"timestamp": "{{timestamp}}",\n\t"notification_id": "{{notification.id}}"\n}\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.PayloadTemplate != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "data.NotificationService.PayloadTemplate")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</textarea><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p></div><div class=\"mb-6\"><label for=\"secret_key\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Secret Key (for signature verification)</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.SecretKey != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<input type=\"text\" id=\"secret_key\" name=\"secret_key\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Optional signature verification key\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.SecretKey)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/webhook.templ`, Line: 87, Col: 417}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">If provided, all webhooks will include an X-GoMFT-Signature header</p></div><div class=\"mb-6\"><label for=\"retry_policy\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Retry Policy</label> <select id=\"retry_policy\" name=\"retry_policy\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.RetryPolicy != "" {
|
||||||
|
if data.NotificationService.RetryPolicy == "none" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<option value=\"none\" selected=\"selected\">No retries</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<option value=\"none\">No retries</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.RetryPolicy == "simple" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<option value=\"simple\" selected=\"selected\">Simple (3 retries)</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<option value=\"simple\">Simple (3 retries)</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.RetryPolicy == "exponential" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<option value=\"exponential\" selected=\"selected\">Exponential backoff</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<option value=\"exponential\">Exponential backoff</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<option value=\"none\">No retries</option> <option value=\"simple\">Simple (3 retries)</option> <option value=\"exponential\">Exponential backoff</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</select></div><!-- Test notification button --><div class=\"mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600\"><div class=\"flex items-center justify-between mb-2\"><h4 class=\"text-base font-medium text-gray-900 dark:text-white\">Test Configuration</h4><button type=\"button\" id=\"test-webhook-btn\" hx-post=\"/admin/settings/notifications/test\" hx-trigger=\"click\" hx-target=\"#test-notification-result\" hx-swap=\"outerHTML\" class=\"px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-paper-plane mr-1\"></i> Send Test Notification</button></div><p class=\"text-sm text-gray-500 dark:text-gray-400\">Send a test notification to verify your configuration works correctly before saving.</p><div id=\"test-notification-result\" class=\"mt-3 hidden\"><!-- Result will be shown here --></div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package form
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/starfleetcptn/gomft/components" // For LayoutWithContext
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/form/utils"
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/form/fields" // Import fields
|
||||||
|
)
|
||||||
|
|
||||||
|
templ NotificationForm(ctx context.Context, data types.NotificationFormData) {
|
||||||
|
@FormScripts() // Include the form-specific scripts
|
||||||
|
@components.LayoutWithContext(utils.GetNotificationFormTitle(data.IsNew), ctx) {
|
||||||
|
<!-- Status and Error Messages (Handled by shared toast component in layout) -->
|
||||||
|
|
||||||
|
<div id="notification-form-container" class="notifications-page bg-gray-50 dark:bg-gray-900 min-h-screen">
|
||||||
|
<div class="pb-8 w-full max-w-4xl mx-auto">
|
||||||
|
<!-- Success Message (hidden, used for HTMX responses) -->
|
||||||
|
if data.SuccessMessage != "" {
|
||||||
|
<div class="hidden success-message">{ data.SuccessMessage }</div>
|
||||||
|
}
|
||||||
|
<!-- Error Message (hidden, used for HTMX responses) -->
|
||||||
|
if data.ErrorMessage != "" {
|
||||||
|
<div class="hidden error-message">{ data.ErrorMessage }</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-bell w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||||
|
{ utils.GetNotificationFormTitle(data.IsNew) }
|
||||||
|
</h1>
|
||||||
|
<a href="/admin/settings/notifications" class="flex items-center justify-center text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg px-5 py-2.5 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 focus:outline-none dark:focus:ring-gray-700">
|
||||||
|
<i class="fas fa-arrow-left w-4 h-4 mr-2"></i>
|
||||||
|
Back to Notification Services
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Notification Service Form -->
|
||||||
|
<div class="mb-6 p-6 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
<form id="notification-form"
|
||||||
|
if data.IsNew {
|
||||||
|
hx-post="/admin/settings/notifications"
|
||||||
|
} else {
|
||||||
|
hx-put={ fmt.Sprintf("/admin/settings/notifications/%d", data.NotificationService.ID) }
|
||||||
|
}
|
||||||
|
hx-target="#notification-form-container">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="notification_type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Notification Type</label>
|
||||||
|
<select id="notification_type" name="type" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option value="">Select a type</option>
|
||||||
|
if data.NotificationService != nil && data.NotificationService.Type == "webhook" {
|
||||||
|
<option value="webhook" selected="selected">Webhook</option>
|
||||||
|
} else {
|
||||||
|
<option value="webhook">Webhook</option>
|
||||||
|
}
|
||||||
|
if data.NotificationService != nil && data.NotificationService.Type == "pushbullet" {
|
||||||
|
<option value="pushbullet" selected="selected">Pushbullet</option>
|
||||||
|
} else {
|
||||||
|
<option value="pushbullet">Pushbullet</option>
|
||||||
|
}
|
||||||
|
if data.NotificationService != nil && data.NotificationService.Type == "ntfy" {
|
||||||
|
<option value="ntfy" selected="selected">Ntfy</option>
|
||||||
|
} else {
|
||||||
|
<option value="ntfy">Ntfy</option>
|
||||||
|
}
|
||||||
|
if data.NotificationService != nil && data.NotificationService.Type == "gotify" {
|
||||||
|
<option value="gotify" selected="selected">Gotify</option>
|
||||||
|
} else {
|
||||||
|
<option value="gotify">Gotify</option>
|
||||||
|
}
|
||||||
|
if data.NotificationService != nil && data.NotificationService.Type == "pushover" {
|
||||||
|
<option value="pushover" selected="selected">Pushover</option>
|
||||||
|
} else {
|
||||||
|
<option value="pushover">Pushover</option>
|
||||||
|
}
|
||||||
|
<option value="email" disabled>Email (Coming Soon)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6 hidden common-fields">
|
||||||
|
<label for="notification_name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Name</label>
|
||||||
|
if data.NotificationService.Name != "" {
|
||||||
|
<input type="text" id="notification_name" name="name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="My Notification Service" required value={ data.NotificationService.Name }/>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="notification_name" name="name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="My Notification Service" required value=""/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mb-6 hidden common-fields">
|
||||||
|
<label for="notification_description" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label>
|
||||||
|
if data.NotificationService.Description != "" {
|
||||||
|
<textarea id="notification_description" name="description" rows="3" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Description for this notification service">{ data.NotificationService.Description }</textarea>
|
||||||
|
} else {
|
||||||
|
<textarea id="notification_description" name="description" rows="3" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Description for this notification service"></textarea>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dynamic fields based on notification type -->
|
||||||
|
@fields.EmailFields(data)
|
||||||
|
@fields.WebhookFields(data)
|
||||||
|
@fields.PushbulletFields(data)
|
||||||
|
@fields.NtfyFields(data)
|
||||||
|
@fields.GotifyFields(data)
|
||||||
|
@fields.PushoverFields(data)
|
||||||
|
|
||||||
|
|
||||||
|
<div class="mb-6 hidden common-fields">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Event Triggers</label>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">Select the job events that should trigger this notification.</p>
|
||||||
|
<div class="flex flex-wrap gap-4">
|
||||||
|
for _, event := range []string{"job_start", "job_complete", "job_error"} {
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input
|
||||||
|
id={ "trigger_" + event }
|
||||||
|
name="event_triggers[]"
|
||||||
|
type="checkbox"
|
||||||
|
value={ event }
|
||||||
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
if utils.IsEventTriggerSelected(data.NotificationService, event, data.IsNew) {
|
||||||
|
checked
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label for={ "trigger_" + event } class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300">{ utils.FormatEventTriggerName(event) }</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start mb-6 hidden common-fields">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<input type="hidden" name="is_enabled" value="false">
|
||||||
|
<input
|
||||||
|
id="is_enabled"
|
||||||
|
name="is_enabled"
|
||||||
|
type="checkbox"
|
||||||
|
value="true"
|
||||||
|
class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"
|
||||||
|
if data.NotificationService != nil && data.NotificationService.IsEnabled {
|
||||||
|
checked
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 text-sm">
|
||||||
|
<label for="is_enabled" class="font-medium text-gray-900 dark:text-white">Enable this notification service</label>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">Check this box to make the service active.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hidden common-fields">
|
||||||
|
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
|
if data.IsNew {
|
||||||
|
Add Service
|
||||||
|
} else {
|
||||||
|
Save Changes
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Help Notice -->
|
||||||
|
<div class="mt-8 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<i class="fas fa-info-circle text-blue-400 dark:text-blue-400"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<p class="text-sm text-blue-700 dark:text-blue-400">
|
||||||
|
Configure your notification service to receive alerts for job events. Different notification types have different configuration options.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Theme-specific background handled by Tailwind classes -->
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package form
|
||||||
|
|
||||||
|
// FormScripts contains JavaScript specific to the notification form page.
|
||||||
|
templ FormScripts() {
|
||||||
|
<script>
|
||||||
|
// Toggle notification fields based on selection
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const typeSelector = document.getElementById('notification_type');
|
||||||
|
// Ensure typeSelector exists before adding listener
|
||||||
|
if (!typeSelector) {
|
||||||
|
console.warn("Notification type selector not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allFields = document.querySelectorAll('.notification-fields');
|
||||||
|
const commonFields = document.querySelectorAll('.common-fields');
|
||||||
|
|
||||||
|
function toggleFields() {
|
||||||
|
// Hide all specific fields first
|
||||||
|
allFields.forEach(field => field.classList.add('hidden'));
|
||||||
|
|
||||||
|
// Show/hide common fields based on selection
|
||||||
|
const selectedType = typeSelector.value;
|
||||||
|
if (selectedType) {
|
||||||
|
// Show common fields (name, description, is_enabled, submit)
|
||||||
|
commonFields.forEach(field => field.classList.remove('hidden'));
|
||||||
|
|
||||||
|
// Show the selected type's specific fields
|
||||||
|
const fieldsToShow = document.getElementById(`${selectedType}_fields`);
|
||||||
|
if (fieldsToShow) {
|
||||||
|
fieldsToShow.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Hide common fields if no type selected
|
||||||
|
commonFields.forEach(field => field.classList.add('hidden'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typeSelector.addEventListener('change', toggleFields);
|
||||||
|
|
||||||
|
// Initialize form state on load (if editing or if a type is pre-selected)
|
||||||
|
toggleFields();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.833
|
||||||
|
package form
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
// FormScripts contains JavaScript specific to the notification form page.
|
||||||
|
func FormScripts() templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<script>\n\t\t// Toggle notification fields based on selection\n\t\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t\tconst typeSelector = document.getElementById('notification_type');\n\t\t\t// Ensure typeSelector exists before adding listener\n\t\t\tif (!typeSelector) {\n\t\t\t\tconsole.warn(\"Notification type selector not found.\");\n\t\t\t\treturn; \n\t\t\t}\n\n\t\t\tconst allFields = document.querySelectorAll('.notification-fields');\n\t\t\tconst commonFields = document.querySelectorAll('.common-fields');\n\n\t\t\tfunction toggleFields() {\n\t\t\t\t// Hide all specific fields first\n\t\t\t\tallFields.forEach(field => field.classList.add('hidden'));\n\n\t\t\t\t// Show/hide common fields based on selection\n\t\t\t\tconst selectedType = typeSelector.value;\n\t\t\t\tif (selectedType) {\n\t\t\t\t\t// Show common fields (name, description, is_enabled, submit)\n\t\t\t\t\tcommonFields.forEach(field => field.classList.remove('hidden'));\n\n\t\t\t\t\t// Show the selected type's specific fields\n\t\t\t\t\tconst fieldsToShow = document.getElementById(`${selectedType}_fields`);\n\t\t\t\t\tif (fieldsToShow) {\n\t\t\t\t\t\tfieldsToShow.classList.remove('hidden');\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Hide common fields if no type selected\n\t\t\t\t\tcommonFields.forEach(field => field.classList.add('hidden'));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttypeSelector.addEventListener('change', toggleFields);\n\n\t\t\t// Initialize form state on load (if editing or if a type is pre-selected)\n\t\t\ttoggleFields(); \n\t\t});\n\t</script>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,398 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.833
|
||||||
|
package form
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/starfleetcptn/gomft/components" // For LayoutWithContext
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/form/fields" // Import fields
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/form/utils"
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NotificationForm(ctx context.Context, data types.NotificationFormData) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = FormScripts().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Status and Error Messages (Handled by shared toast component in layout) --> <div id=\"notification-form-container\" class=\"notifications-page bg-gray-50 dark:bg-gray-900 min-h-screen\"><div class=\"pb-8 w-full max-w-4xl mx-auto\"><!-- Success Message (hidden, used for HTMX responses) -->")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.SuccessMessage != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"hidden success-message\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.SuccessMessage)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 21, Col: 62}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<!-- Error Message (hidden, used for HTMX responses) -->")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.ErrorMessage != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"hidden error-message\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.ErrorMessage)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 25, Col: 58}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4\"><h1 class=\"text-2xl font-bold text-gray-900 dark:text-white flex items-center\"><i class=\"fas fa-bell w-6 h-6 mr-2 text-blue-500 dark:text-blue-400\"></i> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(utils.GetNotificationFormTitle(data.IsNew))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 31, Col: 50}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</h1><a href=\"/admin/settings/notifications\" class=\"flex items-center justify-center text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg px-5 py-2.5 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 focus:outline-none dark:focus:ring-gray-700\"><i class=\"fas fa-arrow-left w-4 h-4 mr-2\"></i> Back to Notification Services</a></div><!-- Add Notification Service Form --><div class=\"mb-6 p-6 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800\"><form id=\"notification-form\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.IsNew {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " hx-post=\"/admin/settings/notifications\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " hx-put=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/admin/settings/notifications/%d", data.NotificationService.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 45, Col: 92}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " hx-target=\"#notification-form-container\"><div class=\"mb-6\"><label for=\"notification_type\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Notification Type</label> <select id=\"notification_type\" name=\"type\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"><option value=\"\">Select a type</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService != nil && data.NotificationService.Type == "webhook" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<option value=\"webhook\" selected=\"selected\">Webhook</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<option value=\"webhook\">Webhook</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if data.NotificationService != nil && data.NotificationService.Type == "pushbullet" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<option value=\"pushbullet\" selected=\"selected\">Pushbullet</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<option value=\"pushbullet\">Pushbullet</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if data.NotificationService != nil && data.NotificationService.Type == "ntfy" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<option value=\"ntfy\" selected=\"selected\">Ntfy</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<option value=\"ntfy\">Ntfy</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if data.NotificationService != nil && data.NotificationService.Type == "gotify" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<option value=\"gotify\" selected=\"selected\">Gotify</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<option value=\"gotify\">Gotify</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if data.NotificationService != nil && data.NotificationService.Type == "pushover" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<option value=\"pushover\" selected=\"selected\">Pushover</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<option value=\"pushover\">Pushover</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<option value=\"email\" disabled>Email (Coming Soon)</option></select></div><div class=\"mb-6 hidden common-fields\"><label for=\"notification_name\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Name</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.Name != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<input type=\"text\" id=\"notification_name\" name=\"name\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"My Notification Service\" required value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 83, Col: 414}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<input type=\"text\" id=\"notification_name\" name=\"name\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"My Notification Service\" required value=\"\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div><div class=\"mb-6 hidden common-fields\"><label for=\"notification_description\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Description</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService.Description != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<textarea id=\"notification_description\" name=\"description\" rows=\"3\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Description for this notification service\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.Description)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 91, Col: 438}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</textarea>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<textarea id=\"notification_description\" name=\"description\" rows=\"3\" class=\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"Description for this notification service\"></textarea>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</div><!-- Dynamic fields based on notification type -->")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = fields.EmailFields(data).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = fields.WebhookFields(data).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = fields.PushbulletFields(data).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = fields.NtfyFields(data).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = fields.GotifyFields(data).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = fields.PushoverFields(data).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<div class=\"mb-6 hidden common-fields\"><label class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Event Triggers</label><p class=\"text-xs text-gray-500 dark:text-gray-400 mb-2\">Select the job events that should trigger this notification.</p><div class=\"flex flex-wrap gap-4\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, event := range []string{"job_start", "job_complete", "job_error"} {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<div class=\"flex items-center\"><input id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 string
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs("trigger_" + event)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 113, Col: 34}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\" name=\"event_triggers[]\" type=\"checkbox\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 string
|
||||||
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(event)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 116, Col: 24}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" class=\"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if utils.IsEventTriggerSelected(data.NotificationService, event, data.IsNew) {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " checked")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "> <label for=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var11 string
|
||||||
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs("trigger_" + event)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 122, Col: 41}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" class=\"ml-2 text-sm font-medium text-gray-900 dark:text-gray-300\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var12 string
|
||||||
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(utils.FormatEventTriggerName(event))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 122, Col: 147}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</label></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</div></div><div class=\"flex items-start mb-6 hidden common-fields\"><div class=\"flex items-center h-5\"><input id=\"is_enabled\" name=\"is_enabled\" type=\"checkbox\" value=\"true\" class=\"w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NotificationService != nil && data.NotificationService.IsEnabled {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, " checked")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "></div><div class=\"ml-3 text-sm\"><label for=\"is_enabled\" class=\"font-medium text-gray-900 dark:text-white\">Enable this notification service</label><p class=\"text-xs text-gray-500 dark:text-gray-400\">Check this box to make the service active.</p></div></div><div class=\"hidden common-fields\"><button type=\"submit\" class=\"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.IsNew {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "Add Service")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "Save Changes")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</button></div></form></div><!-- Help Notice --><div class=\"mt-8 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700\"><div class=\"flex\"><div class=\"flex-shrink-0\"><i class=\"fas fa-info-circle text-blue-400 dark:text-blue-400\"></i></div><div class=\"ml-3\"><p class=\"text-sm text-blue-700 dark:text-blue-400\">Configure your notification service to receive alerts for job events. Different notification types have different configuration options.</p></div></div></div></div></div><!-- Theme-specific background handled by Tailwind classes -->")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = components.LayoutWithContext(utils.GetNotificationFormTitle(data.IsNew), ctx).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetNotificationFormTitle returns the title for the notification form page.
|
||||||
|
func GetNotificationFormTitle(isNew bool) string {
|
||||||
|
if isNew {
|
||||||
|
return "Add Notification Service"
|
||||||
|
}
|
||||||
|
return "Edit Notification Service"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains checks if a string slice contains a specific string.
|
||||||
|
func Contains(slice []string, item string) bool {
|
||||||
|
for _, a := range slice {
|
||||||
|
if a == item {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoolToString converts a boolean to its string representation "true" or "false".
|
||||||
|
// Useful for setting HTML attributes that expect string values.
|
||||||
|
func BoolToString(b bool) string {
|
||||||
|
if b {
|
||||||
|
return "true"
|
||||||
|
}
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEventTriggerSelected checks if a specific event trigger should be pre-selected.
|
||||||
|
// It now accepts the anonymous struct type defined in types.NotificationFormData.
|
||||||
|
// Defaults to checking 'job_complete' and 'job_error' when creating a new service.
|
||||||
|
func IsEventTriggerSelected(service *struct {
|
||||||
|
ID uint
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Type string
|
||||||
|
IsEnabled bool
|
||||||
|
EventTriggers []string
|
||||||
|
RetryPolicy string
|
||||||
|
WebhookURL string
|
||||||
|
Method string
|
||||||
|
Headers string
|
||||||
|
PayloadTemplate string
|
||||||
|
SecretKey string
|
||||||
|
PushbulletAPIKey string
|
||||||
|
PushbulletDeviceID string
|
||||||
|
PushbulletTitleTemplate string
|
||||||
|
PushbulletBodyTemplate string
|
||||||
|
NtfyServer string
|
||||||
|
NtfyTopic string
|
||||||
|
NtfyPriority string
|
||||||
|
NtfyUsername string
|
||||||
|
NtfyPassword string
|
||||||
|
NtfyTitleTemplate string
|
||||||
|
NtfyMessageTemplate string
|
||||||
|
GotifyURL string
|
||||||
|
GotifyToken string
|
||||||
|
GotifyPriority string
|
||||||
|
GotifyTitleTemplate string
|
||||||
|
GotifyMessageTemplate string
|
||||||
|
PushoverAPIToken string
|
||||||
|
PushoverUserKey string
|
||||||
|
PushoverDevice string
|
||||||
|
PushoverPriority string
|
||||||
|
PushoverSound string
|
||||||
|
PushoverTitleTemplate string
|
||||||
|
PushoverMessageTemplate string
|
||||||
|
}, event string, isNew bool) bool {
|
||||||
|
if !isNew && service != nil {
|
||||||
|
// Use the Contains helper function
|
||||||
|
return Contains(service.EventTriggers, event)
|
||||||
|
}
|
||||||
|
// Default for new services: check complete and error
|
||||||
|
return isNew && (event == "job_complete" || event == "job_error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatEventTriggerName converts event trigger keys to human-readable names.
|
||||||
|
func FormatEventTriggerName(event string) string {
|
||||||
|
// Replace underscores with spaces and capitalize words
|
||||||
|
name := strings.ReplaceAll(event, "_", " ")
|
||||||
|
name = strings.Title(name) // Use strings.Title for capitalization
|
||||||
|
return name
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
package list
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/starfleetcptn/gomft/components"
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/dialog"
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// List renders the notification services list page.
|
||||||
|
templ List(ctx context.Context, data types.SettingsNotificationsData) {
|
||||||
|
@components.LayoutWithContext("Notification Services", ctx) {
|
||||||
|
<!-- Status and Error Messages (Handled by shared toast component in layout) -->
|
||||||
|
|
||||||
|
<div id="notifications-container" class="notifications-page bg-gray-50 dark:bg-gray-900 min-h-screen">
|
||||||
|
<div class="pb-8 w-full">
|
||||||
|
<!-- Success Message (hidden, used for HTMX responses/toast trigger) -->
|
||||||
|
if data.SuccessMessage != "" {
|
||||||
|
<div class="hidden success-message">{ data.SuccessMessage }</div>
|
||||||
|
}
|
||||||
|
<!-- Error Message (hidden, used for HTMX responses/toast trigger) -->
|
||||||
|
if data.ErrorMessage != "" {
|
||||||
|
<div class="hidden error-message">{ data.ErrorMessage }</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-bell w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||||
|
Notification Services
|
||||||
|
</h1>
|
||||||
|
<a href="/admin/settings/notifications/new" class="flex items-center justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||||
|
Add Notification Service
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- List of Notification Services -->
|
||||||
|
if len(data.NotificationServices) == 0 {
|
||||||
|
<div class="text-center py-8 bg-white dark:bg-gray-800 shadow-md rounded-lg">
|
||||||
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100 dark:bg-blue-900 mb-4">
|
||||||
|
<i class="fas fa-bell text-2xl text-blue-600 dark:text-blue-400"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">No notification services configured</h3>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 mb-4">Add a notification service to receive alerts for job events.</p>
|
||||||
|
<a href="/admin/settings/notifications/new" class="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||||
|
Add First Notification Service
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 overflow-hidden">
|
||||||
|
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
for _, service := range data.NotificationServices {
|
||||||
|
<li>
|
||||||
|
<div class="block hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||||
|
<div class="px-4 py-4 sm:px-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
if service.Type == "email" {
|
||||||
|
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 dark:bg-blue-900 dark:text-blue-400 mr-3">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
</div>
|
||||||
|
} else if service.Type == "webhook" {
|
||||||
|
<div class="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center text-green-600 dark:bg-green-900 dark:text-green-400 mr-3">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</div>
|
||||||
|
} else { // Default icon
|
||||||
|
<div class="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 dark:bg-gray-700 dark:text-gray-400 mr-3">
|
||||||
|
<i class="fas fa-bell"></i>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-blue-600 dark:text-blue-400 truncate">
|
||||||
|
{ service.Name }
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
{ service.Description }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 flex-shrink-0 flex space-x-2">
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL(fmt.Sprintf("/admin/settings/notifications/%d/edit", service.ID)) }
|
||||||
|
class="text-gray-500 bg-white focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 rounded-lg text-sm p-2 mr-1 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<!-- Add notification delete dialog -->
|
||||||
|
@dialog.NotificationDialog(
|
||||||
|
fmt.Sprintf("delete-notification-dialog-%d", service.ID),
|
||||||
|
"Delete Notification Service",
|
||||||
|
fmt.Sprintf("Are you sure you want to delete the notification service '%s'? This cannot be undone.", service.Name),
|
||||||
|
"text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800",
|
||||||
|
"Delete",
|
||||||
|
"delete",
|
||||||
|
service.ID,
|
||||||
|
service.Name,
|
||||||
|
)
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={ templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-notification-dialog-%d')", service.ID)} }
|
||||||
|
class="text-red-500 bg-white focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 rounded-lg text-sm p-2 dark:bg-gray-800 dark:text-red-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 sm:flex sm:justify-between">
|
||||||
|
<div class="sm:flex flex-col md:flex-row gap-2 md:gap-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span
|
||||||
|
class={ "px-2 py-1 text-xs font-medium rounded-full",
|
||||||
|
templ.KV("bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300", service.IsEnabled),
|
||||||
|
templ.KV("bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300", !service.IsEnabled) }
|
||||||
|
>
|
||||||
|
if service.IsEnabled {
|
||||||
|
Active
|
||||||
|
} else {
|
||||||
|
Disabled
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span class="ml-2 px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300 rounded-full">
|
||||||
|
{ service.Type }
|
||||||
|
</span>
|
||||||
|
if len(service.EventTriggers) > 0 && service.Type == "webhook" {
|
||||||
|
<span class="ml-2 px-2 py-1 text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300 rounded-full">
|
||||||
|
{ fmt.Sprintf("%d triggers", len(service.EventTriggers)) }
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
if service.SuccessCount > 0 || service.FailureCount > 0 {
|
||||||
|
<span class="ml-2 px-2 py-1 text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300 rounded-full">
|
||||||
|
{ fmt.Sprintf("%d/%d", service.SuccessCount, service.SuccessCount + service.FailureCount) }
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
if service.Type == "webhook" {
|
||||||
|
<div class="mt-2 md:mt-0 flex items-center space-x-4">
|
||||||
|
<div class="text-xs">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Events:</span>
|
||||||
|
<span class="ml-1 text-gray-900 dark:text-gray-300">
|
||||||
|
if len(service.EventTriggers) == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
for i, trigger := range service.EventTriggers {
|
||||||
|
if i > 0 {
|
||||||
|
<span>, </span>
|
||||||
|
}
|
||||||
|
{ trigger }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Retry:</span>
|
||||||
|
<span class="ml-1 text-gray-900 dark:text-gray-300">
|
||||||
|
if service.RetryPolicy == "" {
|
||||||
|
Default
|
||||||
|
} else {
|
||||||
|
{ service.RetryPolicy }
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="mt-2 md:mt-0 flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="far fa-clock w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||||
|
<p>
|
||||||
|
Last sent:
|
||||||
|
if service.SuccessCount > 0 {
|
||||||
|
"Recently"
|
||||||
|
} else {
|
||||||
|
"Never"
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Help Notice Placeholder -->
|
||||||
|
<div class="mt-8 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<i class="fas fa-info-circle text-blue-400 dark:text-blue-400"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<p class="text-sm text-blue-700 dark:text-blue-400">
|
||||||
|
Notification services allow the system to send alerts for job events such as completion, errors, or when jobs start.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@dialog.DialogScripts()
|
||||||
|
|
||||||
|
}
|
||||||
|
// Script call removed for now
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
package list
|
||||||
|
|
||||||
|
// ListScripts contains JavaScript specific to the notification list page.
|
||||||
|
templ ListScripts() {
|
||||||
|
<script>
|
||||||
|
// Notification system (showToast function is now in shared/toast/toast_js.templ)
|
||||||
|
|
||||||
|
// Track all HTMX events for debugging
|
||||||
|
document.addEventListener('htmx:beforeRequest', function(event) {
|
||||||
|
// Check if this is a DELETE request for a notification service
|
||||||
|
const path = event.detail.path;
|
||||||
|
const method = event.detail.verb;
|
||||||
|
|
||||||
|
console.log(`Request path: ${path}, method: ${method}`);
|
||||||
|
|
||||||
|
// Pattern match for notification service deletions (e.g., /admin/settings/notifications/123)
|
||||||
|
if (path && method === 'DELETE' && path.match(/^\/admin\/settings\/notifications\/\d+$/)) {
|
||||||
|
console.log("Detected notification service deletion request via URL pattern");
|
||||||
|
|
||||||
|
// This is definitely a delete request - store this information
|
||||||
|
window.isServiceDeleteRequest = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('htmx:afterRequest', function(event) {
|
||||||
|
// Check for notification service deletion multiple ways
|
||||||
|
const isDeleteRequest =
|
||||||
|
// Check global flag from the triggerServiceDelete function
|
||||||
|
window.currentlyDeletingService ||
|
||||||
|
// Check flag from beforeRequest handler
|
||||||
|
window.isServiceDeleteRequest ||
|
||||||
|
// Check URL pattern directly from this event
|
||||||
|
(event.detail.pathInfo &&
|
||||||
|
event.detail.pathInfo.requestPath &&
|
||||||
|
event.detail.pathInfo.requestPath.match(/^\/admin\/settings\/notifications\/\d+$/) &&
|
||||||
|
event.detail.verb === 'DELETE');
|
||||||
|
|
||||||
|
console.log(`Is delete request: ${isDeleteRequest}`);
|
||||||
|
|
||||||
|
// If this is a successful delete request, show notification
|
||||||
|
if (isDeleteRequest && event.detail.successful) {
|
||||||
|
console.log("Delete request was successful");
|
||||||
|
|
||||||
|
let serviceName = "Unknown";
|
||||||
|
|
||||||
|
// Try multiple sources for service name
|
||||||
|
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||||
|
serviceName = event.detail.elt.getAttribute('data-service-name') || serviceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serviceName === "Unknown" && window.lastDeletedService) {
|
||||||
|
// Fallback to our stored service info
|
||||||
|
serviceName = window.lastDeletedService.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Showing success notification for deleted service: ${serviceName}`);
|
||||||
|
// Ensure showToast is globally available
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast(`Notification service "${serviceName}" deleted successfully`, 'success');
|
||||||
|
} else {
|
||||||
|
console.error("showToast function not found!");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Clear flags
|
||||||
|
window.currentlyDeletingService = false;
|
||||||
|
window.isServiceDeleteRequest = false;
|
||||||
|
window.lastDeletedService = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('htmx:responseError', function(event) {
|
||||||
|
console.log("HTMX response error:", event.detail);
|
||||||
|
|
||||||
|
// Similar logic as success but for errors
|
||||||
|
const isDeleteRequest =
|
||||||
|
window.currentlyDeletingService ||
|
||||||
|
window.isServiceDeleteRequest ||
|
||||||
|
(event.detail.pathInfo &&
|
||||||
|
event.detail.pathInfo.requestPath &&
|
||||||
|
event.detail.pathInfo.requestPath.match(/^\/admin\/settings\/notifications\/\d+$/) &&
|
||||||
|
event.detail.verb === 'DELETE');
|
||||||
|
|
||||||
|
let errorMsg = 'An error occurred';
|
||||||
|
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||||
|
errorMsg = event.detail.xhr.responseText;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDeleteRequest) {
|
||||||
|
console.log("Delete request failed");
|
||||||
|
|
||||||
|
let serviceName = "Unknown";
|
||||||
|
|
||||||
|
// Try multiple sources for service name
|
||||||
|
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||||
|
serviceName = event.detail.elt.getAttribute('data-service-name') || serviceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serviceName === "Unknown" && window.lastDeletedService) {
|
||||||
|
// Fallback to our stored service info
|
||||||
|
serviceName = window.lastDeletedService.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
let specificErrorMsg = `Failed to delete notification service "${serviceName}"`;
|
||||||
|
|
||||||
|
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||||
|
// Try to provide a more specific error from the response
|
||||||
|
specificErrorMsg = `Error deleting "${serviceName}": ${event.detail.xhr.responseText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Showing error notification: ${specificErrorMsg}`);
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast(specificErrorMsg, 'error');
|
||||||
|
} else {
|
||||||
|
console.error("showToast function not found!");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Clear flags
|
||||||
|
window.currentlyDeletingService = false;
|
||||||
|
window.isServiceDeleteRequest = false;
|
||||||
|
window.lastDeletedService = null;
|
||||||
|
} else {
|
||||||
|
// General error toast
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast(errorMsg, 'error');
|
||||||
|
} else {
|
||||||
|
console.error("showToast function not found!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle modal hide buttons (This might be better placed globally or in layout if modals are used elsewhere)
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// This listener handles closing modals via data-modal-hide attribute
|
||||||
|
// It might conflict or be redundant if Flowbite's JS handles this already.
|
||||||
|
// Consider removing if Flowbite is initialized globally.
|
||||||
|
const hideButtons = document.querySelectorAll('[data-modal-hide]');
|
||||||
|
hideButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const modalId = this.getAttribute('data-modal-hide');
|
||||||
|
const modal = document.getElementById(modalId);
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
}
|
||||||
|
const backdrop = document.getElementById(modalId + "-backdrop");
|
||||||
|
if (backdrop) {
|
||||||
|
backdrop.classList.add("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show any success or error messages passed via data struct as toasts
|
||||||
|
const successDiv = document.querySelector('.success-message');
|
||||||
|
if (successDiv) {
|
||||||
|
const successMsg = successDiv.textContent.trim();
|
||||||
|
if (successMsg && typeof showToast === 'function') {
|
||||||
|
showToast(successMsg, 'success');
|
||||||
|
} else if (successMsg) {
|
||||||
|
console.error("showToast function not found, cannot display success message:", successMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorDiv = document.querySelector('.error-message');
|
||||||
|
if (errorDiv) {
|
||||||
|
const errorMsg = errorDiv.textContent.trim();
|
||||||
|
if (errorMsg && typeof showToast === 'function') {
|
||||||
|
showToast(errorMsg, 'error');
|
||||||
|
} else if (errorMsg) {
|
||||||
|
console.error("showToast function not found, cannot display error message:", errorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Script for handling the service deletion trigger
|
||||||
|
function triggerServiceDelete(dialogId, serviceID, serviceName) {
|
||||||
|
// Hide the dialog first
|
||||||
|
closeModal(dialogId); // Reuse closeModal logic
|
||||||
|
|
||||||
|
// Add debugging info
|
||||||
|
console.log(`Notification service deletion triggered for: ${serviceName} (ID: ${serviceID})`);
|
||||||
|
|
||||||
|
// Store data in a way that's accessible to event handlers
|
||||||
|
window.lastDeletedService = {
|
||||||
|
id: serviceID,
|
||||||
|
name: serviceName
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add custom marker to track this deletion
|
||||||
|
window.currentlyDeletingService = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Script for closing the modal
|
||||||
|
function closeModal(id) {
|
||||||
|
const dialog = document.getElementById(id);
|
||||||
|
if (dialog) {
|
||||||
|
dialog.classList.add("hidden");
|
||||||
|
dialog.classList.remove("flex");
|
||||||
|
}
|
||||||
|
const backdrop = document.getElementById(id + "-backdrop");
|
||||||
|
if (backdrop) {
|
||||||
|
// Instead of removing, hide it to potentially reuse
|
||||||
|
backdrop.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Script for showing the modal
|
||||||
|
function showModal(id) {
|
||||||
|
const dialog = document.getElementById(id);
|
||||||
|
if (dialog) {
|
||||||
|
dialog.classList.remove("hidden");
|
||||||
|
dialog.classList.add("flex"); // Use flex to center content
|
||||||
|
}
|
||||||
|
const backdrop = document.getElementById(id + "-backdrop");
|
||||||
|
if (backdrop) {
|
||||||
|
backdrop.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,403 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.833
|
||||||
|
package list
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/starfleetcptn/gomft/components"
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/dialog"
|
||||||
|
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// List renders the notification services list page.
|
||||||
|
func List(ctx context.Context, data types.SettingsNotificationsData) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Status and Error Messages (Handled by shared toast component in layout) --> <div id=\"notifications-container\" class=\"notifications-page bg-gray-50 dark:bg-gray-900 min-h-screen\"><div class=\"pb-8 w-full\"><!-- Success Message (hidden, used for HTMX responses/toast trigger) -->")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.SuccessMessage != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"hidden success-message\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.SuccessMessage)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 20, Col: 62}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<!-- Error Message (hidden, used for HTMX responses/toast trigger) -->")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.ErrorMessage != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"hidden error-message\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.ErrorMessage)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 24, Col: 58}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4\"><h1 class=\"text-2xl font-bold text-gray-900 dark:text-white flex items-center\"><i class=\"fas fa-bell w-6 h-6 mr-2 text-blue-500 dark:text-blue-400\"></i> Notification Services</h1><a href=\"/admin/settings/notifications/new\" class=\"flex items-center justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-plus w-4 h-4 mr-2\"></i> Add Notification Service</a></div><!-- List of Notification Services -->")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if len(data.NotificationServices) == 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"text-center py-8 bg-white dark:bg-gray-800 shadow-md rounded-lg\"><div class=\"inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100 dark:bg-blue-900 mb-4\"><i class=\"fas fa-bell text-2xl text-blue-600 dark:text-blue-400\"></i></div><h3 class=\"mb-2 text-lg font-semibold text-gray-900 dark:text-white\">No notification services configured</h3><p class=\"text-gray-500 dark:text-gray-400 mb-4\">Add a notification service to receive alerts for job events.</p><a href=\"/admin/settings/notifications/new\" class=\"inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800\"><i class=\"fas fa-plus w-4 h-4 mr-2\"></i> Add First Notification Service</a></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 overflow-hidden\"><ul class=\"divide-y divide-gray-200 dark:divide-gray-700\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, service := range data.NotificationServices {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<li><div class=\"block hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors\"><div class=\"px-4 py-4 sm:px-6\"><div class=\"flex items-center justify-between\"><div class=\"flex items-center\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if service.Type == "email" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 dark:bg-blue-900 dark:text-blue-400 mr-3\"><i class=\"fas fa-envelope\"></i></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else if service.Type == "webhook" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"w-10 h-10 rounded-full bg-green-100 flex items-center justify-center text-green-600 dark:bg-green-900 dark:text-green-400 mr-3\"><i class=\"fas fa-code\"></i></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " <div class=\"w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 dark:bg-gray-700 dark:text-gray-400 mr-3\"><i class=\"fas fa-bell\"></i></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div><p class=\"text-sm font-medium text-blue-600 dark:text-blue-400 truncate\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(service.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 75, Col: 29}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</p><p class=\"text-sm text-gray-500 dark:text-gray-400 mt-1\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(service.Description)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 78, Col: 36}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</p></div></div><div class=\"ml-2 flex-shrink-0 flex space-x-2\"><a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/admin/settings/notifications/%d/edit", service.ID))
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var7)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" class=\"text-gray-500 bg-white focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 rounded-lg text-sm p-2 mr-1 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700\"><i class=\"fas fa-edit\"></i></a><!-- Add notification delete dialog -->")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = dialog.NotificationDialog(
|
||||||
|
fmt.Sprintf("delete-notification-dialog-%d", service.ID),
|
||||||
|
"Delete Notification Service",
|
||||||
|
fmt.Sprintf("Are you sure you want to delete the notification service '%s'? This cannot be undone.", service.Name),
|
||||||
|
"text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800",
|
||||||
|
"Delete",
|
||||||
|
"delete",
|
||||||
|
service.ID,
|
||||||
|
service.Name,
|
||||||
|
).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-notification-dialog-%d')", service.ID)})
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<button type=\"button\" onclick=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-notification-dialog-%d')", service.ID)}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8.Call)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" class=\"text-red-500 bg-white focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 rounded-lg text-sm p-2 dark:bg-gray-800 dark:text-red-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700\"><i class=\"fas fa-trash-alt\"></i></button></div></div><div class=\"mt-3 sm:flex sm:justify-between\"><div class=\"sm:flex flex-col md:flex-row gap-2 md:gap-6\"><div class=\"flex items-center\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 = []any{"px-2 py-1 text-xs font-medium rounded-full",
|
||||||
|
templ.KV("bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300", service.IsEnabled),
|
||||||
|
templ.KV("bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300", !service.IsEnabled)}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<span class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 string
|
||||||
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var9).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if service.IsEnabled {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "Active")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "Disabled")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</span> <span class=\"ml-2 px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300 rounded-full\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var11 string
|
||||||
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(service.Type)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 124, Col: 29}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</span> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if len(service.EventTriggers) > 0 && service.Type == "webhook" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<span class=\"ml-2 px-2 py-1 text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300 rounded-full\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var12 string
|
||||||
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d triggers", len(service.EventTriggers)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 128, Col: 72}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</span> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if service.SuccessCount > 0 || service.FailureCount > 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<span class=\"ml-2 px-2 py-1 text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300 rounded-full\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 string
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", service.SuccessCount, service.SuccessCount+service.FailureCount))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 133, Col: 105}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if service.Type == "webhook" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<div class=\"mt-2 md:mt-0 flex items-center space-x-4\"><div class=\"text-xs\"><span class=\"text-gray-500 dark:text-gray-400\">Events:</span> <span class=\"ml-1 text-gray-900 dark:text-gray-300\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if len(service.EventTriggers) == 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "None")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i, trigger := range service.EventTriggers {
|
||||||
|
if i > 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<span>, </span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var14 string
|
||||||
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(trigger)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 150, Col: 27}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</span></div><div class=\"text-xs\"><span class=\"text-gray-500 dark:text-gray-400\">Retry:</span> <span class=\"ml-1 text-gray-900 dark:text-gray-300\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if service.RetryPolicy == "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "Default")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var templ_7745c5c3_Var15 string
|
||||||
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(service.RetryPolicy)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 161, Col: 38}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</span></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<div class=\"mt-2 md:mt-0 flex items-center text-sm text-gray-500 dark:text-gray-400\"><i class=\"far fa-clock w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500\"></i><p>Last sent: ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if service.SuccessCount > 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\"Recently\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\"Never\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</p></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</div></div></div></li>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</ul></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<!-- Help Notice Placeholder --><div class=\"mt-8 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700\"><div class=\"flex\"><div class=\"flex-shrink-0\"><i class=\"fas fa-info-circle text-blue-400 dark:text-blue-400\"></i></div><div class=\"ml-3\"><p class=\"text-sm text-blue-700 dark:text-blue-400\">Notification services allow the system to send alerts for job events such as completion, errors, or when jobs start.</p></div></div></div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = dialog.DialogScripts().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = components.LayoutWithContext("Notification Services", ctx).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
// SettingsNotificationsData defines the data needed for the notifications list page
|
||||||
|
type SettingsNotificationsData struct {
|
||||||
|
NotificationServices []NotificationServiceData
|
||||||
|
SuccessMessage string
|
||||||
|
ErrorMessage string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotificationServiceData defines the data for a single service in the list
|
||||||
|
type NotificationServiceData struct {
|
||||||
|
ID uint
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
IsEnabled bool
|
||||||
|
Config map[string]string // Keep for now, might refine later
|
||||||
|
Description string
|
||||||
|
EventTriggers []string
|
||||||
|
PayloadTemplate string
|
||||||
|
SecretKey string
|
||||||
|
RetryPolicy string
|
||||||
|
SuccessCount int
|
||||||
|
FailureCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotificationFormData defines the data needed for the notification add/edit form
|
||||||
|
// TODO: This will be moved from notification_form.templ later
|
||||||
|
type NotificationFormData struct {
|
||||||
|
NotificationService *struct {
|
||||||
|
ID uint
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Type string
|
||||||
|
IsEnabled bool
|
||||||
|
EventTriggers []string
|
||||||
|
RetryPolicy string
|
||||||
|
WebhookURL string
|
||||||
|
Method string
|
||||||
|
Headers string
|
||||||
|
PayloadTemplate string
|
||||||
|
SecretKey string
|
||||||
|
PushbulletAPIKey string
|
||||||
|
PushbulletDeviceID string
|
||||||
|
PushbulletTitleTemplate string
|
||||||
|
PushbulletBodyTemplate string
|
||||||
|
NtfyServer string
|
||||||
|
NtfyTopic string
|
||||||
|
NtfyPriority string
|
||||||
|
NtfyUsername string
|
||||||
|
NtfyPassword string
|
||||||
|
NtfyTitleTemplate string
|
||||||
|
NtfyMessageTemplate string
|
||||||
|
GotifyURL string
|
||||||
|
GotifyToken string
|
||||||
|
GotifyPriority string
|
||||||
|
GotifyTitleTemplate string
|
||||||
|
GotifyMessageTemplate string
|
||||||
|
PushoverAPIToken string
|
||||||
|
PushoverUserKey string
|
||||||
|
PushoverDevice string
|
||||||
|
PushoverPriority string
|
||||||
|
PushoverSound string
|
||||||
|
PushoverTitleTemplate string
|
||||||
|
PushoverMessageTemplate string
|
||||||
|
}
|
||||||
|
IsNew bool
|
||||||
|
SuccessMessage string
|
||||||
|
ErrorMessage string
|
||||||
|
}
|
||||||
+162
-122
@@ -5,55 +5,53 @@ import (
|
|||||||
"github.com/starfleetcptn/gomft/internal/db"
|
"github.com/starfleetcptn/gomft/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Dialog component for 2FA disable confirmation
|
// Dialog component for 2FA disable confirmation with Flowbite styling
|
||||||
templ TwoFactorDisableDialog() {
|
templ TwoFactorDisableDialog() {
|
||||||
<div id="disable-2fa-dialog" class="hidden fixed inset-0 bg-secondary-900/50 dark:bg-secondary-900/80 backdrop-blur-sm z-50 flex items-center justify-center">
|
<div id="disable-2fa-dialog" tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full inset-0 overflow-y-auto overflow-x-hidden bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
|
||||||
<div class="bg-white dark:bg-secondary-800 rounded-lg shadow-xl max-w-md w-full mx-4 overflow-hidden">
|
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
|
||||||
<div class="px-6 pt-5 pb-3 text-center">
|
<div class="relative bg-white rounded-lg shadow-xl dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
|
||||||
<div class="flex justify-center mb-2">
|
<div class="p-5 text-center">
|
||||||
<i class="fas fa-shield-alt text-yellow-400 text-3xl"></i>
|
<div class="h-14 w-14 rounded-full bg-yellow-100 dark:bg-yellow-900 p-2 flex items-center justify-center mx-auto mb-5">
|
||||||
</div>
|
<i class="fas fa-shield-alt text-yellow-500 dark:text-yellow-300 text-xl"></i>
|
||||||
<h3 class="text-xl font-medium text-secondary-900 dark:text-secondary-100">
|
</div>
|
||||||
Disable Two-Factor Authentication
|
<h3 class="mb-5 text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
</h3>
|
Disable Two-Factor Authentication
|
||||||
</div>
|
</h3>
|
||||||
<div class="px-6 py-4">
|
<p class="mb-6 text-gray-500 dark:text-gray-400">
|
||||||
<p class="text-secondary-700 dark:text-secondary-300 mb-4">
|
Are you sure you want to disable two-factor authentication? This will make your account less secure.
|
||||||
Are you sure you want to disable two-factor authentication? This will make your account less secure.
|
</p>
|
||||||
</p>
|
<div class="mb-6">
|
||||||
<div class="space-y-4">
|
<label for="current-password-2fa" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<div>
|
|
||||||
<label for="current-password-2fa" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
|
||||||
<i class="fas fa-lock mr-1"></i> Current Password
|
<i class="fas fa-lock mr-1"></i> Current Password
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="current-password-2fa"
|
id="current-password-2fa"
|
||||||
name="current_password"
|
name="current_password"
|
||||||
class="form-input pl-10 w-full"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
placeholder="Enter your current password"
|
placeholder="Enter your current password"
|
||||||
required/>
|
required/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="disable-2fa-result"></div>
|
<div id="disable-2fa-result" class="mb-5"></div>
|
||||||
|
<div class="flex justify-center space-x-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-white bg-red-600 hover:bg-red-700 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 inline-flex items-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800"
|
||||||
|
onclick="submitDisable2FA()">
|
||||||
|
<i class="fas fa-times mr-2"></i>
|
||||||
|
Disable 2FA
|
||||||
|
</button>
|
||||||
|
<button type="button" class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700" onclick="hideDisable2FADialog()">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-6 py-4 flex justify-end space-x-3">
|
|
||||||
<button type="button" class="btn-secondary" onclick="hideDisable2FADialog()">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn-danger"
|
|
||||||
onclick="submitDisable2FA()">
|
|
||||||
<i class="fas fa-times mr-1"></i>
|
|
||||||
Disable 2FA
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -68,6 +66,7 @@ templ Profile(ctx context.Context, user db.User) {
|
|||||||
// Global functions for dialog control
|
// Global functions for dialog control
|
||||||
window.hideDisable2FADialog = function() {
|
window.hideDisable2FADialog = function() {
|
||||||
document.getElementById('disable-2fa-dialog').classList.add('hidden');
|
document.getElementById('disable-2fa-dialog').classList.add('hidden');
|
||||||
|
document.getElementById('disable-2fa-dialog').classList.remove('flex');
|
||||||
document.getElementById('current-password-2fa').value = '';
|
document.getElementById('current-password-2fa').value = '';
|
||||||
document.getElementById('disable-2fa-result').innerHTML = '';
|
document.getElementById('disable-2fa-result').innerHTML = '';
|
||||||
};
|
};
|
||||||
@@ -75,14 +74,18 @@ templ Profile(ctx context.Context, user db.User) {
|
|||||||
window.showDisable2FADialog = function() {
|
window.showDisable2FADialog = function() {
|
||||||
console.log('Showing 2FA disable dialog');
|
console.log('Showing 2FA disable dialog');
|
||||||
document.getElementById('disable-2fa-dialog').classList.remove('hidden');
|
document.getElementById('disable-2fa-dialog').classList.remove('hidden');
|
||||||
|
document.getElementById('disable-2fa-dialog').classList.add('flex');
|
||||||
};
|
};
|
||||||
|
|
||||||
window.submitDisable2FA = function() {
|
window.submitDisable2FA = function() {
|
||||||
const password = document.getElementById('current-password-2fa').value;
|
const password = document.getElementById('current-password-2fa').value;
|
||||||
if (!password) {
|
if (!password) {
|
||||||
document.getElementById('disable-2fa-result').innerHTML = `
|
document.getElementById('disable-2fa-result').innerHTML = `
|
||||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded" role="alert">
|
<div class="p-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-red-900 dark:text-red-400" role="alert">
|
||||||
<span class="block sm:inline">Current password is required</span>
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-exclamation-circle mr-2"></i>
|
||||||
|
<span>Current password is required</span>
|
||||||
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -111,64 +114,88 @@ templ Profile(ctx context.Context, user db.User) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
@TwoFactorDisableDialog()
|
@TwoFactorDisableDialog()
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div class="p-4 md:p-6 2xl:p-10">
|
||||||
<div class="flex items-center justify-between mb-6">
|
<!-- Profile Header -->
|
||||||
<h1 class="text-3xl font-bold text-secondary-900 dark:text-secondary-100">
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
<i class="fas fa-user-circle mr-2 text-primary-600 dark:text-primary-400"></i>
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-user-circle w-6 h-6 mr-2 text-blue-500"></i>
|
||||||
Profile
|
Profile
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
<!-- Profile Information Card -->
|
<!-- Profile Information Card -->
|
||||||
<div class="card">
|
<div class="bg-white border border-gray-200 rounded-xl shadow-sm dark:border-gray-700 dark:bg-gray-800 transition-all duration-200 hover:shadow-md">
|
||||||
<div class="card-header">
|
<div class="p-5 border-b border-gray-200 dark:border-gray-700">
|
||||||
<h3 class="text-lg font-medium text-secondary-900 dark:text-secondary-100">
|
<div class="flex items-center space-x-3">
|
||||||
<i class="fas fa-id-card mr-2 text-primary-500"></i>
|
<div class="h-10 w-10 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
|
||||||
Profile Information
|
<i class="fas fa-id-card text-blue-600 dark:text-blue-300"></i>
|
||||||
</h3>
|
</div>
|
||||||
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">Personal details and application settings.</p>
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Profile Information
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">Personal details and application settings</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="p-5">
|
||||||
<dl class="space-y-6">
|
<dl class="space-y-6">
|
||||||
<div class="flex flex-col sm:flex-row">
|
<div class="flex flex-col sm:flex-row">
|
||||||
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400 sm:w-1/3 mb-1 sm:mb-0">Email</dt>
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 sm:w-1/3 mb-1 sm:mb-0">Email</dt>
|
||||||
<dd class="text-sm text-secondary-900 dark:text-secondary-100 sm:w-2/3">{ user.Email }</dd>
|
<dd class="text-sm font-medium text-gray-900 dark:text-white sm:w-2/3">{ user.Email }</dd>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col sm:flex-row">
|
<div class="flex flex-col sm:flex-row">
|
||||||
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400 sm:w-1/3 mb-1 sm:mb-0">Role</dt>
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 sm:w-1/3 mb-1 sm:mb-0">Roles</dt>
|
||||||
<dd class="text-sm text-secondary-900 dark:text-secondary-100 sm:w-2/3">
|
<dd class="text-sm font-medium text-gray-900 dark:text-white sm:w-2/3">
|
||||||
if user.GetIsAdmin() {
|
<div class="flex flex-wrap gap-2">
|
||||||
<span class="badge badge-success">
|
if user.GetIsAdmin() {
|
||||||
<i class="fas fa-user-shield mr-1"></i> Administrator
|
<span class="bg-blue-100 text-blue-800 text-xs font-medium px-3 py-1 rounded-full dark:bg-blue-900 dark:text-blue-300 inline-flex items-center">
|
||||||
</span>
|
<i class="fas fa-user-shield w-3 h-3 mr-1.5"></i> Administrator
|
||||||
} else {
|
</span>
|
||||||
<span class="badge badge-info">
|
}
|
||||||
<i class="fas fa-user mr-1"></i> Regular User
|
for _, role := range user.Roles {
|
||||||
</span>
|
<span class="bg-gray-100 text-gray-800 text-xs font-medium px-3 py-1 rounded-full dark:bg-gray-700 dark:text-gray-300 inline-flex items-center">
|
||||||
}
|
<i class="fas fa-user-tag w-3 h-3 mr-1.5"></i> { role.Name }
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col sm:flex-row">
|
<div class="flex flex-col sm:flex-row">
|
||||||
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400 sm:w-1/3 mb-1 sm:mb-0">Two-Factor Authentication</dt>
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 sm:w-1/3 mb-1 sm:mb-0">Permissions</dt>
|
||||||
<dd class="text-sm text-secondary-900 dark:text-secondary-100 sm:w-2/3">
|
<dd class="text-sm font-medium text-gray-900 dark:text-white sm:w-2/3">
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
for _, role := range user.Roles {
|
||||||
|
for _, perm := range role.GetPermissions() {
|
||||||
|
<span class="bg-green-100 text-green-800 text-xs font-medium px-2 py-0.5 rounded dark:bg-green-900 dark:text-green-300">
|
||||||
|
{ perm }
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col sm:flex-row">
|
||||||
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 sm:w-1/3 mb-1 sm:mb-0">Two-Factor Authentication</dt>
|
||||||
|
<dd class="text-sm font-medium text-gray-900 dark:text-white sm:w-2/3">
|
||||||
if user.TwoFactorEnabled {
|
if user.TwoFactorEnabled {
|
||||||
<div class="flex flex-col space-y-3">
|
<div class="flex flex-col space-y-3">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="badge badge-success">
|
<span class="bg-green-100 text-green-800 text-xs font-medium px-3 py-1 rounded-full dark:bg-green-900 dark:text-green-300 inline-flex items-center">
|
||||||
<i class="fas fa-shield-alt mr-1"></i> Enabled
|
<i class="fas fa-shield-alt w-3 h-3 mr-1.5"></i> Enabled
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2 mt-2">
|
||||||
<a href="/profile/2fa/backup-codes" class="btn-secondary btn-sm">
|
<a href="/profile/2fa/backup-codes" class="py-2 px-3 text-xs font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 inline-flex items-center shadow-sm">
|
||||||
<i class="fas fa-key mr-1"></i>
|
<i class="fas fa-key w-3 h-3 mr-1.5"></i>
|
||||||
Manage Backup Codes
|
Manage Backup Codes
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-danger btn-sm"
|
class="text-white bg-red-600 hover:bg-red-700 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-xs px-3 py-2 text-center inline-flex items-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800 shadow-sm"
|
||||||
onclick="showDisable2FADialog()">
|
onclick="showDisable2FADialog()">
|
||||||
<i class="fas fa-times mr-1"></i>
|
<i class="fas fa-times w-3 h-3 mr-1.5"></i>
|
||||||
Disable 2FA
|
Disable 2FA
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -176,13 +203,13 @@ templ Profile(ctx context.Context, user db.User) {
|
|||||||
} else {
|
} else {
|
||||||
<div class="flex flex-col space-y-3">
|
<div class="flex flex-col space-y-3">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="badge badge-warning">
|
<span class="bg-yellow-100 text-yellow-800 text-xs font-medium px-3 py-1 rounded-full dark:bg-yellow-900 dark:text-yellow-300 inline-flex items-center">
|
||||||
<i class="fas fa-shield-alt mr-1"></i> Disabled
|
<i class="fas fa-shield-alt w-3 h-3 mr-1.5"></i> Disabled
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="mt-2">
|
||||||
<a href="/profile/2fa/setup" class="btn-primary btn-sm">
|
<a href="/profile/2fa/setup" class="text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-xs px-3 py-2 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 shadow-sm">
|
||||||
<i class="fas fa-lock mr-1"></i>
|
<i class="fas fa-lock w-3 h-3 mr-1.5"></i>
|
||||||
Enable 2FA
|
Enable 2FA
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,12 +218,12 @@ templ Profile(ctx context.Context, user db.User) {
|
|||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col sm:flex-row">
|
<div class="flex flex-col sm:flex-row">
|
||||||
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400 sm:w-1/3 mb-1 sm:mb-0">Theme</dt>
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 sm:w-1/3 mb-1 sm:mb-0">Theme</dt>
|
||||||
<dd class="text-sm text-secondary-900 dark:text-secondary-100 sm:w-2/3">
|
<dd class="text-sm font-medium text-gray-900 dark:text-white sm:w-2/3">
|
||||||
<form
|
<form
|
||||||
hx-post="/profile/theme"
|
hx-post="/profile/theme"
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
class="flex items-center space-x-4">
|
class="flex items-center space-x-5">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
@@ -206,9 +233,9 @@ templ Profile(ctx context.Context, user db.User) {
|
|||||||
checked?={ user.Theme == "light" || user.Theme == "" }
|
checked?={ user.Theme == "light" || user.Theme == "" }
|
||||||
hx-trigger="change"
|
hx-trigger="change"
|
||||||
hx-post="/profile/theme"
|
hx-post="/profile/theme"
|
||||||
class="form-checkbox" />
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
|
||||||
<label for="theme-light" class="ml-2 block text-sm text-secondary-700 dark:text-secondary-300">
|
<label for="theme-light" class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300">
|
||||||
<i class="fas fa-sun mr-1"></i> Light
|
<i class="fas fa-sun mr-1.5"></i> Light
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -220,9 +247,9 @@ templ Profile(ctx context.Context, user db.User) {
|
|||||||
checked?={ user.Theme == "dark" }
|
checked?={ user.Theme == "dark" }
|
||||||
hx-trigger="change"
|
hx-trigger="change"
|
||||||
hx-post="/profile/theme"
|
hx-post="/profile/theme"
|
||||||
class="form-checkbox" />
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
|
||||||
<label for="theme-dark" class="ml-2 block text-sm text-secondary-700 dark:text-secondary-300">
|
<label for="theme-dark" class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300">
|
||||||
<i class="fas fa-moon mr-1"></i> Dark
|
<i class="fas fa-moon mr-1.5"></i> Dark
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -234,9 +261,9 @@ templ Profile(ctx context.Context, user db.User) {
|
|||||||
checked?={ user.Theme == "system" }
|
checked?={ user.Theme == "system" }
|
||||||
hx-trigger="change"
|
hx-trigger="change"
|
||||||
hx-post="/profile/theme"
|
hx-post="/profile/theme"
|
||||||
class="form-checkbox" />
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
|
||||||
<label for="theme-system" class="ml-2 block text-sm text-secondary-700 dark:text-secondary-300">
|
<label for="theme-system" class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300">
|
||||||
<i class="fas fa-desktop mr-1"></i> System
|
<i class="fas fa-desktop mr-1.5"></i> System
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -247,15 +274,21 @@ templ Profile(ctx context.Context, user db.User) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Change Password Card -->
|
<!-- Change Password Card -->
|
||||||
<div class="card">
|
<div class="bg-white border border-gray-200 rounded-xl shadow-sm dark:border-gray-700 dark:bg-gray-800 transition-all duration-200 hover:shadow-md">
|
||||||
<div class="card-header">
|
<div class="p-5 border-b border-gray-200 dark:border-gray-700">
|
||||||
<h3 class="text-lg font-medium text-secondary-900 dark:text-secondary-100">
|
<div class="flex items-center space-x-3">
|
||||||
<i class="fas fa-key mr-2 text-primary-500"></i>
|
<div class="h-10 w-10 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
|
||||||
Change Password
|
<i class="fas fa-key text-blue-600 dark:text-blue-300"></i>
|
||||||
</h3>
|
</div>
|
||||||
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">Update your password to keep your account secure.</p>
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Change Password
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">Update your password to keep your account secure</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="p-5">
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="/change-password"
|
action="/change-password"
|
||||||
@@ -264,7 +297,7 @@ templ Profile(ctx context.Context, user db.User) {
|
|||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
hx-headers='{"X-Profile-Page": "true"}'
|
hx-headers='{"X-Profile-Page": "true"}'
|
||||||
hx-indicator="#password-change-indicator"
|
hx-indicator="#password-change-indicator"
|
||||||
class="space-y-4"
|
class="space-y-5"
|
||||||
x-data="{
|
x-data="{
|
||||||
currentPassword: '',
|
currentPassword: '',
|
||||||
newPassword: '',
|
newPassword: '',
|
||||||
@@ -283,57 +316,57 @@ templ Profile(ctx context.Context, user db.User) {
|
|||||||
<div id="password-result"></div>
|
<div id="password-result"></div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="current-password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<label for="current-password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<i class="fas fa-lock mr-1"></i> Current Password
|
<i class="fas fa-lock mr-1.5"></i> Current Password
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="current-password"
|
id="current-password"
|
||||||
name="current_password"
|
name="current_password"
|
||||||
x-model="currentPassword"
|
x-model="currentPassword"
|
||||||
class="form-input pl-10 w-full"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
required/>
|
required/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="new-password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<label for="new-password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<i class="fas fa-lock-open mr-1"></i> New Password
|
<i class="fas fa-lock-open mr-1.5"></i> New Password
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="new-password"
|
id="new-password"
|
||||||
name="new_password"
|
name="new_password"
|
||||||
x-model="newPassword"
|
x-model="newPassword"
|
||||||
class="form-input pl-10 w-full"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
required/>
|
required/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="confirm-password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<label for="confirm-password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<i class="fas fa-check-double mr-1"></i> Confirm New Password
|
<i class="fas fa-check-double mr-1.5"></i> Confirm New Password
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="confirm-password"
|
id="confirm-password"
|
||||||
name="confirm_password"
|
name="confirm_password"
|
||||||
x-model="confirmPassword"
|
x-model="confirmPassword"
|
||||||
class="form-input pl-10 w-full"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
required/>
|
required/>
|
||||||
</div>
|
</div>
|
||||||
@@ -342,13 +375,13 @@ templ Profile(ctx context.Context, user db.User) {
|
|||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn-primary w-full flex justify-center py-3"
|
class="w-full text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 shadow-sm"
|
||||||
x-bind:disabled="!validate() || loading">
|
x-bind:disabled="!validate() || loading">
|
||||||
<span x-show="!loading" class="flex items-center">
|
<span x-show="!loading" class="flex items-center justify-center">
|
||||||
<i class="fas fa-save mr-2"></i>
|
<i class="fas fa-save mr-2"></i>
|
||||||
Update Password
|
Update Password
|
||||||
</span>
|
</span>
|
||||||
<span x-show="loading" class="flex items-center">
|
<span x-show="loading" class="flex items-center justify-center">
|
||||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
@@ -359,9 +392,11 @@ templ Profile(ctx context.Context, user db.User) {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-6 py-4 bg-secondary-50 dark:bg-secondary-800 border-t border-secondary-200 dark:border-secondary-700">
|
<div class="p-5 bg-gray-50 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 rounded-b-xl">
|
||||||
<div class="flex items-center text-sm text-secondary-500 dark:text-secondary-400">
|
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||||
<i class="fas fa-shield-alt mr-2 text-primary-500"></i>
|
<div class="mr-3 text-blue-500 flex-shrink-0">
|
||||||
|
<i class="fas fa-shield-alt"></i>
|
||||||
|
</div>
|
||||||
<span>Password must be at least 8 characters with letters, numbers, and special characters</span>
|
<span>Password must be at least 8 characters with letters, numbers, and special characters</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -369,10 +404,15 @@ templ Profile(ctx context.Context, user db.User) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Security Notice -->
|
<!-- Security Notice -->
|
||||||
<div class="mt-8 text-center">
|
<div class="mt-8">
|
||||||
<div class="inline-flex items-center text-sm text-secondary-500 dark:text-secondary-400">
|
<div class="p-5 text-sm text-blue-800 rounded-xl bg-blue-50 border border-blue-200 dark:bg-blue-900/50 dark:text-blue-400 dark:border-blue-800 flex items-start shadow-sm">
|
||||||
<i class="fas fa-shield-alt mr-2 text-primary-500"></i>
|
<div class="mr-3 text-blue-500 dark:text-blue-400 flex-shrink-0 mt-0.5">
|
||||||
<span>All profile changes are securely logged for your protection</span>
|
<i class="fas fa-shield-alt"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-medium mb-1">Security Information</h4>
|
||||||
|
<p>All profile changes are securely logged for your protection. For security reasons, you'll be required to enter your password for sensitive changes.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,63 +1,66 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/starfleetcptn/gomft/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
templ NameField() {
|
templ NameField() {
|
||||||
<div class="space-y-6">
|
<div class="mb-6">
|
||||||
<div class="sm:col-span-4">
|
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Configuration Name</label>
|
||||||
<label for="name" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Name</label>
|
<div class="relative">
|
||||||
<div class="relative">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<i class="fas fa-tag text-gray-400 dark:text-gray-500"></i>
|
||||||
<i class="fas fa-tag text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input type="text" name="name" id="name" x-model="name" required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="Transfer configuration name" />
|
|
||||||
</div>
|
</div>
|
||||||
|
<input type="text" name="name" id="name" x-model="name" required
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="Transfer configuration name" />
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Choose a descriptive name to identify this configuration.</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ FilePatternFields() {
|
templ FilePatternFields() {
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="file_pattern" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">File
|
<label for="file_pattern" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">File Pattern</label>
|
||||||
Pattern</label>
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-filter text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-filter text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" name="file_pattern" id="file_pattern" x-model="filePattern"
|
<input type="text" name="file_pattern" id="file_pattern" x-model="filePattern"
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
placeholder="*.txt, *.csv" />
|
placeholder="*.txt, *.csv" />
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Glob pattern for files to transfer. Leave empty to transfer all files.
|
Glob pattern for files to transfer. Leave empty to transfer all files.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="output_pattern"
|
<label for="output_pattern" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Output Pattern</label>
|
||||||
class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Output Pattern</label>
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-file-export text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-file-export text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" name="output_pattern" id="output_pattern" x-model="outputPattern"
|
<input type="text" name="output_pattern" id="output_pattern" x-model="outputPattern"
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
placeholder="${filename}" />
|
placeholder="${filename}" />
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Pattern for output filename. Use variables like ${`filename`}, ${`timestamp`}, ${`date`}
|
Pattern for output filename. Use variables like ${`filename`}, ${`timestamp`}, ${`date`}
|
||||||
</p>
|
</p>
|
||||||
<div>
|
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<div class="mt-4 p-4 bg-gray-50 rounded-lg border border-gray-200 dark:bg-gray-800 dark:border-gray-700">
|
||||||
Pattern for filenames. Available variables:<br />
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-2">Available variables:</h4>
|
||||||
${`filename`} - Original filename without extension (e.g., "report")<br />
|
<p class="text-sm text-gray-500 dark:text-gray-400 space-y-1">
|
||||||
${`ext`} - Original file extension (e.g., "csv")<br />
|
<span class="block">• ${`filename`} - Original filename without extension (e.g., "report")</span>
|
||||||
${`date:format`} - Current date using Go's time format:<br />
|
<span class="block">• ${`ext`} - Original file extension (e.g., "csv")</span>
|
||||||
• 2006-01-02 → YYYY-MM-DD<br />
|
<span class="block">• ${`date:format`} - Current date using Go's time format:</span>
|
||||||
• 20060102 → YYYYMMDD<br />
|
<span class="block pl-4">- 2006-01-02 → YYYY-MM-DD</span>
|
||||||
• 2006-01-02 15:04:05 → YYYY-MM-DD_HH:MM:SS<br />
|
<span class="block pl-4">- 20060102 → YYYYMMDD</span>
|
||||||
Example: ${`filename`}_${`date:2006-01-02`}_${`ext`} → "report_2023-03-01.csv"
|
<span class="block pl-4">- 2006-01-02 15:04:05 → YYYY-MM-DD_HH:MM:SS</span>
|
||||||
|
<span class="block italic mt-2">Example: ${`filename`}_${`date:2006-01-02`}_${`ext`} → "report_2023-03-01.csv"</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,127 +68,129 @@ templ FilePatternFields() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
templ ArchiveOptions() {
|
templ ArchiveOptions() {
|
||||||
<div class="border border-secondary-200 dark:border-secondary-700 rounded-lg p-6 bg-secondary-50 dark:bg-secondary-800/50 shadow-sm">
|
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
||||||
<h3 class="text-lg font-medium text-secondary-900 dark:text-secondary-100 mb-4">Archive & Delete Options</h3>
|
<div class="space-y-6">
|
||||||
|
<div class="flex items-center">
|
||||||
<div class="mb-4">
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
<label for="archive_enabled" class="flex items-center cursor-pointer">
|
<input type="checkbox" id="archive_enabled" name="archive_enabled" x-model="archiveEnabled"
|
||||||
<div class="relative">
|
class="sr-only peer" :value="archiveEnabled ? 'true' : 'false'">
|
||||||
<input id="archive_enabled" name="archive_enabled" type="checkbox" x-model="archiveEnabled"
|
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||||
class="sr-only"
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-white">Enable archiving</span>
|
||||||
:value="archiveEnabled ? 'true' : 'false'"
|
|
||||||
/>
|
|
||||||
<div class="block bg-secondary-200 dark:bg-secondary-700 w-14 h-8 rounded-full"></div>
|
|
||||||
<div class="dot absolute left-1 top-1 bg-white dark:bg-secondary-100 w-6 h-6 rounded-full transition"
|
|
||||||
:class="archiveEnabled ? 'transform translate-x-6 bg-primary-500' : ''"></div>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 text-secondary-700 dark:text-secondary-300 font-medium">
|
|
||||||
Enable archiving
|
|
||||||
</div>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4" x-show="archiveEnabled">
|
<div x-show="archiveEnabled" class="mt-4">
|
||||||
<label for="archive_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Archive Path</label>
|
<label for="archive_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Archive Path</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<span class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-folder text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-folder text-gray-400 dark:text-gray-500"></i>
|
||||||
</span>
|
</div>
|
||||||
<input id="archive_path" name="archive_path" type="text"
|
<input id="archive_path" name="archive_path" type="text" x-model="archivePath"
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
placeholder="Path to archive files"
|
placeholder="Path to archive files" />
|
||||||
x-model="archivePath"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Files will be moved here after successful transfer
|
Files will be moved here after successful transfer
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="flex items-center">
|
||||||
<label for="delete_after_transfer" class="flex items-center cursor-pointer">
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
<div class="relative">
|
<input type="checkbox" id="delete_after_transfer" name="delete_after_transfer" x-model="deleteAfterTransfer"
|
||||||
<input id="delete_after_transfer" name="delete_after_transfer" type="checkbox" x-model="deleteAfterTransfer"
|
class="sr-only peer" :value="deleteAfterTransfer ? 'true' : 'false'">
|
||||||
class="sr-only"
|
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-red-300 dark:peer-focus:ring-red-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-red-600"></div>
|
||||||
:value="deleteAfterTransfer ? 'true' : 'false'"
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-white">Delete source files after transfer</span>
|
||||||
/>
|
|
||||||
<div class="block bg-secondary-200 dark:bg-secondary-700 w-14 h-8 rounded-full"></div>
|
|
||||||
<div class="dot absolute left-1 top-1 bg-white dark:bg-secondary-100 w-6 h-6 rounded-full transition"
|
|
||||||
:class="deleteAfterTransfer ? 'transform translate-x-6 bg-red-500' : ''"></div>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 text-secondary-700 dark:text-secondary-300 font-medium">
|
|
||||||
Delete source files after transfer
|
|
||||||
</div>
|
|
||||||
</label>
|
</label>
|
||||||
<p class="mt-1 ml-14 text-xs text-red-500 dark:text-red-400" x-show="deleteAfterTransfer">
|
</div>
|
||||||
<i class="fas fa-exclamation-triangle mr-1"></i> Warning: This will permanently delete the original files
|
<div x-show="deleteAfterTransfer" class="mt-2">
|
||||||
</p>
|
<div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-red-900/30 dark:text-red-300" role="alert">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-2"></i>
|
||||||
|
<span>Warning: This will permanently delete the original files</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="flex items-center">
|
||||||
<label for="skip_processed_files" class="flex items-center cursor-pointer">
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
<div class="relative">
|
<input type="checkbox" id="skip_processed_files" name="skip_processed_files" x-model="skipProcessedFiles"
|
||||||
<input id="skip_processed_files" name="skip_processed_files" type="checkbox" x-model="skipProcessedFiles"
|
class="sr-only peer" :value="skipProcessedFiles ? 'true' : 'false'">
|
||||||
class="sr-only"
|
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||||
:value="skipProcessedFiles ? 'true' : 'false'"
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-white">Skip files that have already been processed</span>
|
||||||
/>
|
|
||||||
<div class="block bg-secondary-200 dark:bg-secondary-700 w-14 h-8 rounded-full"></div>
|
|
||||||
<div class="dot absolute left-1 top-1 bg-white dark:bg-secondary-100 w-6 h-6 rounded-full transition"
|
|
||||||
:class="skipProcessedFiles ? 'transform translate-x-6 bg-primary-500' : ''"></div>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 text-secondary-700 dark:text-secondary-300 font-medium">
|
|
||||||
Skip files that have already been processed
|
|
||||||
</div>
|
|
||||||
</label>
|
</label>
|
||||||
<p class="mt-1 ml-14 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Files with the same hash that have been successfully processed before will be skipped
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="ms-14 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Files with the same hash that have been successfully processed before will be skipped
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mt-6">
|
||||||
<label for="max_concurrent_transfers" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Concurrent Transfers</label>
|
<label for="max_concurrent_transfers" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<div class="flex items-center">
|
Concurrent Transfers: <span x-text="maxConcurrentTransfers"></span>
|
||||||
<input type="range" id="max_concurrent_transfers" name="max_concurrent_transfers"
|
</label>
|
||||||
class="form-range w-2/3 h-2 bg-secondary-200 dark:bg-secondary-700 rounded-lg appearance-none cursor-pointer"
|
<input type="range" id="max_concurrent_transfers" name="max_concurrent_transfers"
|
||||||
min="1" max="32" step="1"
|
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||||
x-model="maxConcurrentTransfers"
|
min="1" max="32" step="1" x-model="maxConcurrentTransfers">
|
||||||
/>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<span class="ml-4 text-secondary-700 dark:text-secondary-300" x-text="maxConcurrentTransfers"></span>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Number of files to transfer simultaneously (higher values may improve performance but increase resource usage)
|
Number of files to transfer simultaneously (higher values may improve performance but increase resource usage)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ RcloneFlags() {
|
templ RcloneFlags(currentCommandID uint) {
|
||||||
<div>
|
<div class="mb-6">
|
||||||
<label for="rclone_flags" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300">Rclone Flags</label>
|
<label for="command_id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Rclone Command</label>
|
||||||
<div class="mt-1 relative rounded-md shadow-sm">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-flag text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-terminal text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
@RcloneCommandOptions(currentCommandID) // Pass it down
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Select the rclone command to use for this configuration.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Flag container is now rendered directly in ConfigForm -->
|
||||||
|
|
||||||
|
<label for="rclone_flags" class="block mb-2 mt-6 text-sm font-medium text-gray-900 dark:text-white">Additional Rclone Flags</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-flag text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" name="rclone_flags" id="rclone_flags" x-model="rcloneFlags"
|
<input type="text" name="rclone_flags" id="rclone_flags" x-model="rcloneFlags"
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
@change="updateCommandRequirements()"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
placeholder="--transfers 4 --checkers 8" />
|
placeholder="--transfers 4 --checkers 8" />
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Optional: Additional rclone flags for fine-tuning the transfer.
|
Optional: Additional rclone flags for fine-tuning the transfer.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New placeholder templ for rclone command options
|
||||||
|
templ RcloneCommandOptions(currentCommandID uint) { // Accept currentCommandID
|
||||||
|
<div
|
||||||
|
hx-get="/api/rclone/commands"
|
||||||
|
hx-trigger="load"
|
||||||
|
hx-target="this"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-vals={ fmt.Sprintf(`{"commandId": %d}`, currentCommandID) }>
|
||||||
|
<!-- Loading placeholder -->
|
||||||
|
<option value="">Loading commands...</option>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
templ SourceSelection() {
|
templ SourceSelection() {
|
||||||
<div class="sm:col-span-3">
|
<div class="mb-6">
|
||||||
<label for="source_type" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Source
|
<label for="source_type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Source Type</label>
|
||||||
Type</label>
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-server text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<select id="source_type" name="source_type" x-model="sourceType"
|
<select id="source_type" name="source_type" x-model="sourceType"
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500">
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
<option value="local">Local</option>
|
<option value="local">Local</option>
|
||||||
<option value="sftp">SFTP</option>
|
<option value="sftp">SFTP</option>
|
||||||
<option value="ftp">FTP</option>
|
<option value="ftp">FTP</option>
|
||||||
@@ -194,23 +199,22 @@ templ SourceSelection() {
|
|||||||
<option value="smb">SMB</option>
|
<option value="smb">SMB</option>
|
||||||
<option value="nextcloud">NextCloud</option>
|
<option value="nextcloud">NextCloud</option>
|
||||||
<option value="webdav">WebDAV</option>
|
<option value="webdav">WebDAV</option>
|
||||||
<option value="gdrive">Google Drive</option>
|
<option value="gdrive">Google Drive (BETA)</option>
|
||||||
<option value="gphotos">Google Photos</option>
|
<option value="gphotos">Google Photos (BETA)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ DestinationSelection() {
|
templ DestinationSelection() {
|
||||||
<div class="sm:col-span-3">
|
<div class="mb-6">
|
||||||
<label for="destination_type"
|
<label for="destination_type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Destination Type</label>
|
||||||
class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Destination Type</label>
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-server text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<select id="destination_type" name="destination_type" x-model="destinationType"
|
<select id="destination_type" name="destination_type" x-model="destinationType"
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500">
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
<option value="local">Local</option>
|
<option value="local">Local</option>
|
||||||
<option value="sftp">SFTP</option>
|
<option value="sftp">SFTP</option>
|
||||||
<option value="ftp">FTP</option>
|
<option value="ftp">FTP</option>
|
||||||
@@ -219,9 +223,246 @@ templ DestinationSelection() {
|
|||||||
<option value="smb">SMB</option>
|
<option value="smb">SMB</option>
|
||||||
<option value="nextcloud">NextCloud</option>
|
<option value="nextcloud">NextCloud</option>
|
||||||
<option value="webdav">WebDAV</option>
|
<option value="webdav">WebDAV</option>
|
||||||
<option value="gdrive">Google Drive</option>
|
<option value="gdrive">Google Drive (BETA)</option>
|
||||||
<option value="gphotos">Google Photos</option>
|
<option value="gphotos">Google Photos (BETA)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// RcloneCommandOptionsContent renders the command options organized by category
|
||||||
|
templ RcloneCommandOptionsContent(categoryMap map[string][]db.RcloneCommand, categories []string, currentCommandID uint, commandFlagsJSON string, commandFlagValuesJSON string) { // Add flag JSON strings
|
||||||
|
<select id="command_id" name="command_id" x-model="commandId"
|
||||||
|
hx-get="/api/rclone/command-flags"
|
||||||
|
hx-target="#command-flags-container"
|
||||||
|
hx-trigger="change"
|
||||||
|
hx-include="[name='command_id']"
|
||||||
|
hx-vals={ fmt.Sprintf(`{"commandFlags": %s, "commandFlagValues": %s}`, commandFlagsJSON, commandFlagValuesJSON) }
|
||||||
|
@change="updateCommandRequirements()"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option value="">Select command...</option>
|
||||||
|
for _, category := range categories {
|
||||||
|
if commands, ok := categoryMap[category]; ok && len(commands) > 0 {
|
||||||
|
<optgroup label={ category }>
|
||||||
|
for _, cmd := range commands {
|
||||||
|
if cmd.ID == currentCommandID {
|
||||||
|
<option value={ fmt.Sprintf("%d", cmd.ID) } selected>{ cmd.Name } - { cmd.Description }</option>
|
||||||
|
} else {
|
||||||
|
<option value={ fmt.Sprintf("%d", cmd.ID) }>{ cmd.Name } - { cmd.Description }</option>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</optgroup>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
}
|
||||||
|
|
||||||
|
// RcloneCommandFlagsContent renders the command flags for a selected command
|
||||||
|
templ RcloneCommandFlagsContent(command *db.RcloneCommand, selectedFlagsMap map[uint]bool, selectedFlagValues map[uint]string) {
|
||||||
|
if command == nil {
|
||||||
|
<div class="p-4 text-red-500">Command not found</div>
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="font-medium text-gray-900 dark:text-white mb-2">{ command.Name }</h4>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">{ command.Description }</p>
|
||||||
|
|
||||||
|
if len(command.Flags) > 0 {
|
||||||
|
<div class="mb-3">
|
||||||
|
<h5 class="font-medium text-gray-900 dark:text-white mb-2">Command Flags:</h5>
|
||||||
|
<div class="space-y-3">
|
||||||
|
for _, flag := range command.Flags {
|
||||||
|
if flag.DataType == "bool" {
|
||||||
|
<!-- Boolean flag (simple checkbox) -->
|
||||||
|
<div class="flex items-start mb-4">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={ fmt.Sprintf("flag_%d", flag.ID) }
|
||||||
|
name="command_flags"
|
||||||
|
value={ fmt.Sprintf("%d", flag.ID) }
|
||||||
|
class="mt-0.5 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:focus:ring-blue-600"
|
||||||
|
if selectedFlagsMap[flag.ID] {
|
||||||
|
checked
|
||||||
|
} />
|
||||||
|
<div class="ml-3">
|
||||||
|
<label for={ fmt.Sprintf("flag_%d", flag.ID) } class="font-medium text-gray-900 dark:text-white">
|
||||||
|
{ flag.Name } - { flag.Description }
|
||||||
|
</label>
|
||||||
|
if flag.DefaultValue != "" {
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">Default: { flag.DefaultValue }</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<!-- Non-boolean flag (requires value) -->
|
||||||
|
<div class="flex items-start mb-4 w-full">
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={ fmt.Sprintf("flag_enable_%d", flag.ID) }
|
||||||
|
name={ fmt.Sprintf("flag_enable_%d", flag.ID) }
|
||||||
|
class="mr-2 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:focus:ring-blue-600"
|
||||||
|
data-input-id={ fmt.Sprintf("flag_value_%d", flag.ID) }
|
||||||
|
onclick="toggleFlagValue(this)"
|
||||||
|
if selectedFlagsMap[flag.ID] {
|
||||||
|
checked
|
||||||
|
} />
|
||||||
|
<label for={ fmt.Sprintf("flag_enable_%d", flag.ID) } class="font-medium text-gray-900 dark:text-white">
|
||||||
|
{ flag.Name } - { flag.Description }
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full mt-2">
|
||||||
|
@renderFlagInput(flag, selectedFlagValues[flag.ID], selectedFlagsMap[flag.ID]) // Pass value and enabled status
|
||||||
|
if flag.DefaultValue != "" {
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Default: { flag.DefaultValue }</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JavaScript to handle enabling/disabling flag value inputs -->
|
||||||
|
<script>
|
||||||
|
function toggleFlagValue(checkbox) {
|
||||||
|
const inputId = checkbox.getAttribute('data-input-id');
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
input.disabled = !checkbox.checked;
|
||||||
|
if (checkbox.checked) {
|
||||||
|
// Focus on the input when enabled
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also enable/disable the hidden input for flag ID
|
||||||
|
const hiddenId = inputId.replace('flag_value_', 'flag_hidden_');
|
||||||
|
const hiddenInput = document.getElementById(hiddenId);
|
||||||
|
if (hiddenInput) {
|
||||||
|
hiddenInput.disabled = !checkbox.checked;
|
||||||
|
console.log('Toggle hidden input:', hiddenId, checkbox.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For debugging
|
||||||
|
console.log('Toggle flag value:', inputId, checkbox.checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize all flag inputs on page load
|
||||||
|
// Initialization is now handled by server-side rendering
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mt-4 p-3 bg-blue-50 text-blue-800 rounded-lg border border-blue-100 dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-900 text-sm">
|
||||||
|
<div class="flex items-center mb-1">
|
||||||
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
|
<span class="font-medium">Usage Examples</span>
|
||||||
|
</div>
|
||||||
|
if command.Name == "ls" ||
|
||||||
|
command.Name == "lsd" ||
|
||||||
|
command.Name == "lsl" ||
|
||||||
|
command.Name == "lsjson" ||
|
||||||
|
command.Name == "lsf" ||
|
||||||
|
command.Name == "delete" ||
|
||||||
|
command.Name == "purge" ||
|
||||||
|
command.Name == "rmdirs" ||
|
||||||
|
command.Name == "mkdir" ||
|
||||||
|
command.Name == "touch" ||
|
||||||
|
command.Name == "md5sum" ||
|
||||||
|
command.Name == "sha1sum" ||
|
||||||
|
command.Name == "sha256sum" ||
|
||||||
|
command.Name == "size" ||
|
||||||
|
command.Name == "stat" ||
|
||||||
|
command.Name == "version" {
|
||||||
|
<code class="block mt-1 font-mono text-xs overflow-x-auto">rclone { command.Name } [flags] source:path</code>
|
||||||
|
} else {
|
||||||
|
<code class="block mt-1 font-mono text-xs overflow-x-auto">rclone { command.Name } [flags] source:path dest:path</code>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="p-3 bg-gray-50 text-gray-600 rounded-lg border border-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-700 text-sm">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
|
<span>This command doesn't have any specific flags.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to render appropriate input based on flag data type
|
||||||
|
templ renderFlagInput(flag db.RcloneCommandFlag, value string, enabled bool) {
|
||||||
|
if flag.DataType == "int" {
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id={ fmt.Sprintf("flag_value_%d", flag.ID) }
|
||||||
|
name={ fmt.Sprintf("flag_value_%d", flag.ID) }
|
||||||
|
class="w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder={ flag.DefaultValue }
|
||||||
|
value={ value }
|
||||||
|
if !enabled {
|
||||||
|
disabled
|
||||||
|
} />
|
||||||
|
<!-- Hidden input to include this flag ID when checked -->
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="command_flags"
|
||||||
|
value={ fmt.Sprintf("%d", flag.ID) }
|
||||||
|
if !enabled {
|
||||||
|
disabled
|
||||||
|
}
|
||||||
|
id={ fmt.Sprintf("flag_hidden_%d", flag.ID) }
|
||||||
|
data-enable-with={ fmt.Sprintf("flag_enable_%d", flag.ID) }
|
||||||
|
/>
|
||||||
|
} else if flag.DataType == "float" {
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id={ fmt.Sprintf("flag_value_%d", flag.ID) }
|
||||||
|
name={ fmt.Sprintf("flag_value_%d", flag.ID) }
|
||||||
|
step="0.01"
|
||||||
|
class="w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder={ flag.DefaultValue }
|
||||||
|
value={ value }
|
||||||
|
if !enabled {
|
||||||
|
disabled
|
||||||
|
} />
|
||||||
|
<!-- Hidden input to include this flag ID when checked -->
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="command_flags"
|
||||||
|
value={ fmt.Sprintf("%d", flag.ID) }
|
||||||
|
if !enabled {
|
||||||
|
disabled
|
||||||
|
}
|
||||||
|
id={ fmt.Sprintf("flag_hidden_%d", flag.ID) }
|
||||||
|
data-enable-with={ fmt.Sprintf("flag_enable_%d", flag.ID) }
|
||||||
|
/>
|
||||||
|
} else {
|
||||||
|
<!-- Default to text input for string and other types -->
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id={ fmt.Sprintf("flag_value_%d", flag.ID) }
|
||||||
|
name={ fmt.Sprintf("flag_value_%d", flag.ID) }
|
||||||
|
class="w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder={ flag.DefaultValue }
|
||||||
|
value={ value }
|
||||||
|
if !enabled {
|
||||||
|
disabled
|
||||||
|
} />
|
||||||
|
<!-- Hidden input to include this flag ID when checked -->
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="command_flags"
|
||||||
|
value={ fmt.Sprintf("%d", flag.ID) }
|
||||||
|
if !enabled {
|
||||||
|
disabled
|
||||||
|
}
|
||||||
|
id={ fmt.Sprintf("flag_hidden_%d", flag.ID) }
|
||||||
|
data-enable-with={ fmt.Sprintf("flag_enable_%d", flag.ID) }
|
||||||
|
/>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,109 +1,81 @@
|
|||||||
package destination
|
package destination
|
||||||
|
|
||||||
templ FTPDestinationForm() {
|
templ FTPDestinationForm() {
|
||||||
<div class="sm:col-span-6 space-y-6">
|
<div class="space-y-6 mt-4">
|
||||||
<div class="sm:col-span-4">
|
<div>
|
||||||
<label for="dest_host" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Host</label>
|
<label for="dest_host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">FTP Host</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-server text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="dest_host" name="dest_host" x-model="destHost" x-bind:required="requiresDestination"
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="dest_host"
|
placeholder="ftp.example.com" />
|
||||||
id="dest_host"
|
|
||||||
x-model="destHost"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="ftp.example.com"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Hostname or IP address of the FTP server</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div>
|
||||||
<label for="dest_port" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Port</label>
|
<label for="dest_port" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Port</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-plug text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-network-wired text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="number" id="dest_port" name="dest_port" x-model="destPort"
|
||||||
type="number"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="dest_port"
|
placeholder="21" min="1" max="65535"
|
||||||
id="dest_port"
|
x-init="if (!destPort || destPort === 0) destPort = 21"
|
||||||
x-model="destPort"
|
x-effect="if (destinationType === 'ftp' && (destPort === 0 || destPort === 22)) destPort = 21" />
|
||||||
required
|
|
||||||
min="1"
|
|
||||||
max="65535"
|
|
||||||
value="21"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="21"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">FTP port number (default: 21)</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div>
|
||||||
<label for="destination_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Remote Path</label>
|
<label for="dest_user" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-folder text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="destination_path"
|
placeholder="username" />
|
||||||
id="destination_path"
|
|
||||||
x-model="destinationPath"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="/path/to/destination"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">FTP username for authentication</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div>
|
||||||
<label for="dest_user" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Username</label>
|
<label for="dest_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-user text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="password" id="dest_password" name="dest_password" x-model="destPassword" x-bind:required="requiresDestination"
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="dest_user"
|
placeholder="Enter password" />
|
||||||
id="dest_user"
|
|
||||||
x-model="destUser"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">FTP password for authentication</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="flex items-center mb-6">
|
||||||
<label for="dest_password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Password</label>
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
<div class="relative">
|
<input type="checkbox" id="dest_passive_mode" name="dest_passive_mode" x-model="destPassiveMode"
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
class="sr-only peer" :value="destPassiveMode ? 'true' : 'false'">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||||
</div>
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-white">Use Passive Mode</span>
|
||||||
<input
|
</label>
|
||||||
type="password"
|
|
||||||
name="dest_password"
|
|
||||||
id="dest_password"
|
|
||||||
x-model="destPassword"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="Password"/>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" name="dest_password" :value="destPassword"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-start mb-4">
|
<div>
|
||||||
<div class="flex items-center h-5">
|
<label for="destination_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Remote Path</label>
|
||||||
<input
|
<div class="relative">
|
||||||
id="dest_passive_mode"
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
name="dest_passive_mode"
|
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||||
type="checkbox"
|
</div>
|
||||||
x-model="destPassiveMode"
|
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath" x-bind:required="requiresDestination"
|
||||||
:value="destPassiveMode ? 'true' : 'false'"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
class="focus:ring-primary-500 h-4 w-4 text-primary-600 border-secondary-300 dark:border-secondary-700 rounded">
|
placeholder="/remote/path/to/files" />
|
||||||
</div>
|
|
||||||
<div class="ml-3 text-sm">
|
|
||||||
<label for="dest_passive_mode" class="font-medium text-secondary-700 dark:text-secondary-300">Use Passive Mode</label>
|
|
||||||
<p class="text-secondary-500 dark:text-secondary-400">Enable passive mode for FTP connection</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Full path to the directory on the FTP server</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1,175 +1,106 @@
|
|||||||
package destination
|
package destination
|
||||||
|
|
||||||
templ GoogleDriveDestinationForm() {
|
templ GoogleDriveDestinationForm() {
|
||||||
<div class="space-y-6" x-init="$watch('useBuiltinAuthDest', value => {
|
<div class="space-y-6 mt-4">
|
||||||
if(value) {
|
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
|
||||||
destClientId = '';
|
<div class="flex items-center">
|
||||||
destClientSecret = '';
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
}
|
<span>Google Drive requires authentication after creating this configuration. You'll be prompted to authenticate once the configuration is saved.</span>
|
||||||
})">
|
</div>
|
||||||
<div class="mb-6">
|
</div>
|
||||||
<label for="use_builtin_auth_dest" class="flex items-center cursor-pointer">
|
|
||||||
<div class="relative">
|
|
||||||
<input id="use_builtin_auth_dest" name="use_builtin_auth_dest" type="checkbox"
|
|
||||||
class="sr-only"
|
|
||||||
x-model="useBuiltinAuthDest"
|
|
||||||
:value="useBuiltinAuthDest ? 'true' : 'false'"
|
|
||||||
/>
|
|
||||||
<div class="block bg-secondary-200 dark:bg-secondary-700 w-14 h-8 rounded-full"></div>
|
|
||||||
<div class="dot absolute left-1 top-1 bg-white dark:bg-secondary-100 w-6 h-6 rounded-full transition"
|
|
||||||
:class="useBuiltinAuthDest ? 'transform translate-x-6 bg-primary-500' : ''"></div>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 text-secondary-700 dark:text-secondary-300 font-medium">
|
|
||||||
Use rclone's built-in Google authentication (recommended)
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<p class="mt-1 ml-14 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Simple one-click authentication using rclone's shared credentials
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div x-bind:class="{ 'opacity-50': useBuiltinAuthDest }">
|
<div>
|
||||||
<div>
|
<div class="flex items-center mb-6">
|
||||||
<label for="dest_client_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
Google Client ID
|
<input type="checkbox" id="use_builtin_auth_dest" name="use_builtin_auth_dest" x-model="useBuiltinAuthDest"
|
||||||
<span x-show="useBuiltinAuthDest" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
class="sr-only peer" :value="useBuiltinAuthDest ? 'true' : 'false'">
|
||||||
</label>
|
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||||
<div class="relative">
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-white">Use built-in authentication</span>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
</label>
|
||||||
<i class="fas fa-id-card text-secondary-400 dark:text-secondary-600"></i>
|
</div>
|
||||||
</div>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destClientId"
|
Toggle to use your own Google API credentials instead of the built-in ones.
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
</p>
|
||||||
x-bind:disabled="useBuiltinAuthDest"
|
</div>
|
||||||
placeholder="Google Drive OAuth Client ID" />
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Client ID from Google Cloud Console
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
<div x-show="!useBuiltinAuthDest">
|
||||||
<label for="dest_client_secret" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
|
||||||
Google Client Secret
|
<div class="flex">
|
||||||
<span x-show="useBuiltinAuthDest" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
<i class="fas fa-exclamation-triangle mr-2 flex-shrink-0"></i>
|
||||||
</label>
|
<span>You'll need to create your own Google API project with Drive API enabled and create OAuth credentials. Learn how at <a href="https://developers.google.com/drive/api/quickstart/go" class="font-medium underline hover:text-yellow-700 dark:hover:text-yellow-200" target="_blank">Google API documentation</a>.</span>
|
||||||
<div class="relative">
|
</div>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
</div>
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destClientSecret"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
x-bind:disabled="useBuiltinAuthDest"
|
|
||||||
placeholder="Google Drive OAuth Client Secret" />
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Client Secret from Google Cloud Console
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mb-6">
|
||||||
<label for="dest_drive_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Folder ID (Optional)</label>
|
<label for="dest_client_id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Client ID</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-folder text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-id-card text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" id="dest_drive_id" name="dest_drive_id" x-model="destDriveId"
|
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destClientId"
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
placeholder="Google Drive Folder ID (optional)" />
|
placeholder="Your Google API Client ID" x-bind:required="!useBuiltinAuthDest" />
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">OAuth 2.0 Client ID from Google Cloud Console</p>
|
||||||
Folder ID to use as the root (leave empty for "My Drive")
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mb-6">
|
||||||
<label for="dest_team_drive" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Shared/Team Drive ID (Optional)</label>
|
<label for="dest_client_secret" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Client Secret</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-users text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" id="dest_team_drive" name="dest_team_drive" x-model="destTeamDrive"
|
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destClientSecret"
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
placeholder="Shared/Team Drive ID (optional)" />
|
placeholder="Your Google API Client Secret" x-bind:required="!useBuiltinAuthDest" />
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">OAuth 2.0 Client Secret from Google Cloud Console</p>
|
||||||
ID of the Shared Drive / Team Drive to use
|
</div>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
<div>
|
||||||
<label for="destination_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Path</label>
|
<label for="destination_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Drive Path</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-folder-open text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fab fa-google-drive text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath"
|
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath"
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
placeholder="Path within Google Drive (e.g., /backup)" />
|
placeholder="/path/in/drive" />
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Path within the Drive where files will be uploaded
|
Path to the directory in your Google Drive. Leave empty for the root of your Drive.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 bg-amber-50 dark:bg-amber-900/50 rounded-lg border border-amber-100 dark:border-amber-700">
|
<div>
|
||||||
<div class="flex">
|
<label for="dest_drive_id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Drive ID (Optional)</label>
|
||||||
<div class="flex-shrink-0">
|
<div class="relative">
|
||||||
<i class="fas fa-exclamation-triangle text-amber-500 dark:text-amber-400"></i>
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
</div>
|
<i class="fas fa-fingerprint text-gray-400 dark:text-gray-500"></i>
|
||||||
<div class="ml-3">
|
</div>
|
||||||
<h3 class="text-sm font-medium text-amber-800 dark:text-amber-200">Authentication Required</h3>
|
<input type="text" id="dest_drive_id" name="dest_drive_id" x-model="destDriveId"
|
||||||
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
<p>After saving this configuration, you'll need to authenticate with Google Drive:</p>
|
placeholder="Specific Google Drive ID" />
|
||||||
<ol class="list-decimal list-inside mt-2 space-y-1">
|
</div>
|
||||||
<li>Go to the configurations page</li>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<li>Click the "Authenticate" button for this configuration</li>
|
For accessing a shared drive. Leave empty to use your personal Google Drive.
|
||||||
<li>You'll be redirected to Google to grant access permissions</li>
|
</p>
|
||||||
<li>After granting permission, you'll be automatically redirected back to GoMFT</li>
|
</div>
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-4 bg-blue-50 dark:bg-blue-900/50 rounded-lg border border-blue-100 dark:border-blue-700">
|
<div>
|
||||||
<div class="flex">
|
<label for="dest_team_drive" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Team Drive (Optional)</label>
|
||||||
<div class="flex-shrink-0">
|
<div class="relative">
|
||||||
<i class="fas fa-info-circle text-blue-400 dark:text-blue-300"></i>
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
</div>
|
<i class="fas fa-users text-gray-400 dark:text-gray-500"></i>
|
||||||
<div class="ml-3">
|
</div>
|
||||||
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">Authentication Information</h3>
|
<input type="text" id="dest_team_drive" name="dest_team_drive" x-model="destTeamDrive"
|
||||||
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
<template x-if="useBuiltinAuthDest">
|
placeholder="Team Drive ID" />
|
||||||
<div>
|
</div>
|
||||||
<p>You're using rclone's built-in authentication, which simplifies the setup process:</p>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<ul class="list-disc list-inside mt-1 space-y-1">
|
For accessing a shared team drive. Leave empty for personal drive.
|
||||||
<li>No need to create your own Google API credentials</li>
|
</p>
|
||||||
<li>Authentication happens through Google's secure OAuth flow</li>
|
</div>
|
||||||
<li>Automatic redirection back to GoMFT after authentication</li>
|
</div>
|
||||||
</ul>
|
|
||||||
<p class="mt-2 text-xs text-amber-600 dark:text-amber-400">
|
|
||||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
|
||||||
Note: The built-in authentication uses shared credentials which have rate limits across all rclone users.
|
|
||||||
If you plan to transfer large amounts of data or run many concurrent transfers, consider creating your own credentials.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template x-if="!useBuiltinAuthDest">
|
|
||||||
<div>
|
|
||||||
<p>To use Google Drive with your own credentials:</p>
|
|
||||||
<ol class="list-decimal list-inside mt-1 space-y-1">
|
|
||||||
<li>Go to the <a href="https://console.cloud.google.com/" target="_blank" class="text-blue-600 dark:text-blue-400 underline">Google Cloud Console</a></li>
|
|
||||||
<li>Create a project and enable the Google Drive API</li>
|
|
||||||
<li>Create OAuth 2.0 credentials (Client ID & Secret)</li>
|
|
||||||
<li>Set authorized redirect URI to <code class="bg-blue-100 dark:bg-blue-800 px-1 py-0.5 rounded">http://localhost:53682/</code></li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
@@ -1,216 +1,188 @@
|
|||||||
package destination
|
package destination
|
||||||
|
|
||||||
templ GooglePhotosDestinationForm() {
|
templ GooglePhotosDestinationForm() {
|
||||||
<div class="space-y-6" x-init="$watch('useBuiltinAuthDest', value => {
|
<div class="space-y-6 mt-4" x-init="$watch('useBuiltinAuthDest', value => {
|
||||||
if(value) {
|
if(value) {
|
||||||
destClientId = '';
|
destClientId = '';
|
||||||
destClientSecret = '';
|
destClientSecret = '';
|
||||||
}
|
}
|
||||||
})">
|
})">
|
||||||
<div class="mb-6">
|
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
|
||||||
<label for="use_builtin_auth_dest" class="flex items-center cursor-pointer">
|
<div class="flex items-center">
|
||||||
<div class="relative">
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
<input id="use_builtin_auth_dest" name="use_builtin_auth_dest" type="checkbox"
|
<span>Google Photos requires authentication after creating this configuration. You'll be prompted to authenticate once the configuration is saved.</span>
|
||||||
class="sr-only"
|
</div>
|
||||||
x-model="useBuiltinAuthDest"
|
</div>
|
||||||
:value="useBuiltinAuthDest ? 'true' : 'false'"
|
|
||||||
/>
|
|
||||||
<div class="block bg-secondary-200 dark:bg-secondary-700 w-14 h-8 rounded-full"></div>
|
|
||||||
<div class="dot absolute left-1 top-1 bg-white dark:bg-secondary-100 w-6 h-6 rounded-full transition"
|
|
||||||
:class="useBuiltinAuthDest ? 'transform translate-x-6 bg-primary-500' : ''"></div>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 text-secondary-700 dark:text-secondary-300 font-medium">
|
|
||||||
Use rclone's built-in Google authentication (recommended)
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<p class="mt-1 ml-14 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Simple one-click authentication using rclone's shared credentials
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div x-bind:class="{ 'opacity-50': useBuiltinAuthDest }">
|
<div>
|
||||||
<div>
|
<div class="flex items-center mb-6">
|
||||||
<label for="dest_client_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
Google Client ID
|
<input type="checkbox" id="use_builtin_auth_dest" name="use_builtin_auth_dest" x-model="useBuiltinAuthDest"
|
||||||
<span x-show="useBuiltinAuthDest" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
class="sr-only peer" :value="useBuiltinAuthDest ? 'true' : 'false'">
|
||||||
</label>
|
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||||
<div class="relative">
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-white">Use rclone's built-in Google authentication (recommended)</span>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
</label>
|
||||||
<i class="fas fa-id-card text-secondary-400 dark:text-secondary-600"></i>
|
</div>
|
||||||
</div>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destClientId"
|
Simple one-click authentication using rclone's shared credentials
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
</p>
|
||||||
x-bind:disabled="useBuiltinAuthDest"
|
</div>
|
||||||
placeholder="Google Photos OAuth Client ID" />
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Client ID from Google Cloud Console
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
<div x-bind:class="{ 'opacity-50': useBuiltinAuthDest }">
|
||||||
<label for="dest_client_secret" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<div class="mb-6">
|
||||||
Google Client Secret
|
<label for="dest_client_id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<span x-show="useBuiltinAuthDest" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
Google Client ID
|
||||||
</label>
|
<span x-show="useBuiltinAuthDest" class="text-gray-500 dark:text-gray-400 text-xs font-normal">(Using rclone default)</span>
|
||||||
<div class="relative">
|
</label>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="relative">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
</div>
|
<i class="fas fa-id-card text-gray-400 dark:text-gray-500"></i>
|
||||||
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destClientSecret"
|
</div>
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destClientId"
|
||||||
x-bind:disabled="useBuiltinAuthDest"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
placeholder="Google Photos OAuth Client Secret" />
|
x-bind:disabled="useBuiltinAuthDest"
|
||||||
</div>
|
placeholder="Google Photos OAuth Client ID" />
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
</div>
|
||||||
Client Secret from Google Cloud Console
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
</p>
|
Client ID from Google Cloud Console
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mb-6">
|
||||||
<label for="dest_read_only" class="flex items-center cursor-pointer">
|
<label for="dest_client_secret" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<div class="relative">
|
Google Client Secret
|
||||||
<input id="dest_read_only" name="dest_read_only" type="checkbox"
|
<span x-show="useBuiltinAuthDest" class="text-gray-500 dark:text-gray-400 text-xs font-normal">(Using rclone default)</span>
|
||||||
class="sr-only"
|
</label>
|
||||||
x-model="destReadOnly"
|
<div class="relative">
|
||||||
:value="destReadOnly ? 'true' : 'false'"
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
/>
|
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
|
||||||
<div class="block bg-secondary-200 dark:bg-secondary-700 w-14 h-8 rounded-full"></div>
|
</div>
|
||||||
<div class="dot absolute left-1 top-1 bg-white dark:bg-secondary-100 w-6 h-6 rounded-full transition"
|
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destClientSecret"
|
||||||
:class="destReadOnly ? 'transform translate-x-6 bg-primary-500' : ''"></div>
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
</div>
|
x-bind:disabled="useBuiltinAuthDest"
|
||||||
<div class="ml-3 text-secondary-700 dark:text-secondary-300 font-medium">
|
placeholder="Google Photos OAuth Client Secret" />
|
||||||
Read-only mode
|
</div>
|
||||||
</div>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
</label>
|
Client Secret from Google Cloud Console
|
||||||
<p class="mt-1 ml-14 text-xs text-secondary-500 dark:text-secondary-400">
|
</p>
|
||||||
Only request read-only access to your photos
|
</div>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="flex items-center mb-6">
|
||||||
<label for="dest_start_year" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Start Year (Optional)</label>
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
<div class="relative">
|
<input type="checkbox" id="dest_read_only" name="dest_read_only" x-model="destReadOnly"
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
class="sr-only peer" :value="destReadOnly ? 'true' : 'false'">
|
||||||
<i class="fas fa-calendar text-secondary-400 dark:text-secondary-600"></i>
|
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||||
</div>
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-white">Read-only mode</span>
|
||||||
<input type="number" id="dest_start_year" name="dest_start_year" x-model="destStartYear"
|
</label>
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
</div>
|
||||||
placeholder="Only include photos after this year (default: 2000)" min="1900" max="2100" />
|
<p class="ms-14 text-sm text-gray-500 dark:text-gray-400">
|
||||||
</div>
|
Only request read-only access to your photos
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
</p>
|
||||||
Only include photos uploaded after this year
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mb-6">
|
||||||
<label for="dest_include_archived" class="flex items-center cursor-pointer">
|
<label for="dest_start_year" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Start Year (Optional)</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input id="dest_include_archived" name="dest_include_archived" type="checkbox"
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
class="sr-only"
|
<i class="fas fa-calendar text-gray-400 dark:text-gray-500"></i>
|
||||||
x-model="destIncludeArchived"
|
</div>
|
||||||
:value="destIncludeArchived ? 'true' : 'false'"
|
<input type="number" id="dest_start_year" name="dest_start_year" x-model="destStartYear"
|
||||||
/>
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
<div class="block bg-secondary-200 dark:bg-secondary-700 w-14 h-8 rounded-full"></div>
|
placeholder="Only include photos after this year (default: 2000)" min="1900" max="2100" />
|
||||||
<div class="dot absolute left-1 top-1 bg-white dark:bg-secondary-100 w-6 h-6 rounded-full transition"
|
</div>
|
||||||
:class="destIncludeArchived ? 'transform translate-x-6 bg-primary-500' : ''"></div>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
</div>
|
Only include photos uploaded after this year
|
||||||
<div class="ml-3 text-secondary-700 dark:text-secondary-300 font-medium">
|
</p>
|
||||||
Include archived media
|
</div>
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<p class="mt-1 ml-14 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Include archived photos and videos in media listings
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="flex items-center mb-6">
|
||||||
<label for="destination_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Album Path</label>
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
<div class="relative">
|
<input type="checkbox" id="dest_include_archived" name="dest_include_archived" x-model="destIncludeArchived"
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
class="sr-only peer" :value="destIncludeArchived ? 'true' : 'false'">
|
||||||
<i class="fas fa-images text-secondary-400 dark:text-secondary-600"></i>
|
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||||
</div>
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-white">Include archived media</span>
|
||||||
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath"
|
</label>
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
</div>
|
||||||
placeholder="Album path (e.g., album/my-photos)" />
|
<p class="ms-14 text-sm text-gray-500 dark:text-gray-400">
|
||||||
</div>
|
Include archived photos and videos in media listings
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
</p>
|
||||||
Path within Google Photos where files will be uploaded.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-4 bg-amber-50 dark:bg-amber-900/50 rounded-lg border border-amber-100 dark:border-amber-700">
|
<div class="mb-6">
|
||||||
<div class="flex">
|
<label for="destination_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Album Path</label>
|
||||||
<div class="flex-shrink-0">
|
<div class="relative">
|
||||||
<i class="fas fa-exclamation-triangle text-amber-500 dark:text-amber-400"></i>
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
</div>
|
<i class="fas fa-images text-gray-400 dark:text-gray-500"></i>
|
||||||
<div class="ml-3">
|
</div>
|
||||||
<h3 class="text-sm font-medium text-amber-800 dark:text-amber-200">Authentication Required</h3>
|
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath"
|
||||||
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
<p>After saving this configuration, you'll need to authenticate with Google Photos:</p>
|
placeholder="Album path (e.g., album/my-photos)" />
|
||||||
<ol class="list-decimal list-inside mt-2 space-y-1">
|
</div>
|
||||||
<li>Go to the configurations page</li>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<li>Click the "Authenticate" button for this configuration</li>
|
Path within Google Photos to download files from
|
||||||
<li>You'll be redirected to Google to grant access permissions</li>
|
</p>
|
||||||
<li>After granting permission, you'll be automatically redirected back to GoMFT</li>
|
</div>
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-4 bg-blue-50 dark:bg-blue-900/50 rounded-lg border border-blue-100 dark:border-blue-700">
|
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-shrink-0">
|
<i class="fas fa-exclamation-triangle mr-2 flex-shrink-0"></i>
|
||||||
<i class="fas fa-info-circle text-blue-400 dark:text-blue-300"></i>
|
<div>
|
||||||
</div>
|
<h3 class="font-medium">Authentication Required</h3>
|
||||||
<div class="ml-3">
|
<p class="mt-1">After saving this configuration, you'll need to authenticate with Google Photos:</p>
|
||||||
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">Authentication Information</h3>
|
<ol class="list-decimal list-inside mt-2 space-y-1">
|
||||||
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
|
<li>Go to the configurations page</li>
|
||||||
<template x-if="useBuiltinAuthDest">
|
<li>Click the "Authenticate" button for this configuration</li>
|
||||||
<div>
|
<li>You'll be redirected to Google to grant access permissions</li>
|
||||||
<p>You're using rclone's built-in authentication, which simplifies the setup process:</p>
|
<li>After granting permission, you'll be automatically redirected back to GoMFT</li>
|
||||||
<ul class="list-disc list-inside mt-1 space-y-1">
|
</ol>
|
||||||
<li>No need to create your own Google API credentials</li>
|
</div>
|
||||||
<li>Authentication happens through Google's secure OAuth flow</li>
|
</div>
|
||||||
<li>Automatic redirection back to GoMFT after authentication</li>
|
</div>
|
||||||
</ul>
|
|
||||||
<p class="mt-2 text-xs text-amber-600 dark:text-amber-400">
|
|
||||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
|
||||||
Note: The built-in authentication uses shared credentials which have rate limits across all rclone users.
|
|
||||||
If you plan to transfer large amounts of data or run many concurrent transfers, consider creating your own credentials.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template x-if="!useBuiltinAuthDest">
|
|
||||||
<div>
|
|
||||||
<p>To use Google Photos with your own credentials:</p>
|
|
||||||
<ol class="list-decimal list-inside mt-1 space-y-1">
|
|
||||||
<li>Go to the <a href="https://console.cloud.google.com/" target="_blank" class="text-blue-600 dark:text-blue-400 underline">Google Cloud Console</a></li>
|
|
||||||
<li>Create a project and enable the Google Photos API</li>
|
|
||||||
<li>Create OAuth 2.0 credentials (Client ID & Secret)</li>
|
|
||||||
<li>Set authorized redirect URI to <code class="bg-blue-100 dark:bg-blue-800 px-1 py-0.5 rounded">http://localhost:53682/</code></li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-4 bg-yellow-50 dark:bg-yellow-900/50 rounded-lg border border-yellow-100 dark:border-yellow-700">
|
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-shrink-0">
|
<i class="fas fa-info-circle mr-2 flex-shrink-0"></i>
|
||||||
<i class="fas fa-exclamation-circle text-yellow-500 dark:text-yellow-400"></i>
|
<div>
|
||||||
</div>
|
<h3 class="font-medium">Authentication Information</h3>
|
||||||
<div class="ml-3">
|
<div class="mt-1">
|
||||||
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">Important Note About Google Photos</h3>
|
<template x-if="useBuiltinAuthDest">
|
||||||
<div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
|
<div>
|
||||||
<p>All media items uploaded to Google Photos with rclone are stored in full resolution at original quality. These uploads will count towards storage in your Google Account.</p>
|
<p>You're using rclone's built-in authentication, which simplifies the setup process:</p>
|
||||||
</div>
|
<ul class="list-disc list-inside mt-1 space-y-1">
|
||||||
</div>
|
<li>No need to create your own Google API credentials</li>
|
||||||
</div>
|
<li>Authentication happens through Google's secure OAuth flow</li>
|
||||||
</div>
|
<li>Automatic redirection back to GoMFT after authentication</li>
|
||||||
</div>
|
</ul>
|
||||||
|
<p class="mt-2 text-yellow-700 dark:text-yellow-300">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-1"></i>
|
||||||
|
Note: The built-in authentication uses shared credentials which have rate limits across all rclone users.
|
||||||
|
If you plan to transfer large amounts of data or run many concurrent transfers, consider creating your own credentials.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-if="!useBuiltinAuthDest">
|
||||||
|
<div>
|
||||||
|
<p>To use Google Photos with your own credentials:</p>
|
||||||
|
<ol class="list-decimal list-inside mt-1 space-y-1">
|
||||||
|
<li>Go to the <a href="https://console.cloud.google.com/" target="_blank" class="font-medium underline hover:text-blue-700 dark:hover:text-blue-300">Google Cloud Console</a></li>
|
||||||
|
<li>Create a project and enable the Google Photos API</li>
|
||||||
|
<li>Create OAuth 2.0 credentials (Client ID & Secret)</li>
|
||||||
|
<li>Set authorized redirect URI to <code class="bg-blue-100 dark:bg-blue-800 px-1.5 py-0.5 rounded text-xs">http://localhost:53682/</code></li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
|
||||||
|
<div class="flex">
|
||||||
|
<i class="fas fa-exclamation-circle mr-2 flex-shrink-0"></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium">Important Note About Google Photos</h3>
|
||||||
|
<p class="mt-1">When downloading from Google Photos, be aware that some original metadata may not be preserved. Google Photos processes and may compress some images upon upload.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1,39 +1,41 @@
|
|||||||
package destination
|
package destination
|
||||||
|
|
||||||
templ LocalDestinationForm() {
|
templ LocalDestinationForm() {
|
||||||
<div class="sm:col-span-6 space-y-6">
|
<div class="space-y-4 mt-4">
|
||||||
<div class="sm:col-span-4">
|
<div>
|
||||||
<label for="destination_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Local Path</label>
|
<label for="destination_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Local Path</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-folder-open text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath" x-bind:required="requiresDestination"
|
||||||
|
:class="{ 'border-red-500 dark:border-red-700': destPathValid === false, 'border-green-500 dark:border-green-700': destPathValid === true }"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="/path/to/files" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Full path to the local directory containing your files</p>
|
||||||
|
<div x-show="destPathValid === false" class="mt-2 text-sm text-red-600 dark:text-red-400">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-exclamation-circle mr-2"></i>
|
||||||
|
<span x-text="destPathError"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div x-show="destPathValid === true" class="mt-2 text-sm text-green-600 dark:text-green-400">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-check-circle mr-2"></i>
|
||||||
|
<span x-text="destPathError || 'Path is valid'"></span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="destination_path"
|
|
||||||
id="destination_path"
|
|
||||||
x-model="destinationPath"
|
|
||||||
required
|
|
||||||
:class="{ 'border-red-300 dark:border-red-700': destPathValid === false, 'border-green-300 dark:border-green-700': destPathValid === true }"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="Local directory path (e.g., /path/to/destination)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Full path to the local directory where files will be saved
|
|
||||||
</p>
|
|
||||||
<!-- Path validation message -->
|
|
||||||
<template x-if="destPathValid !== null">
|
|
||||||
<p x-show="destPathError" class="mt-1 text-sm" :class="destPathValid ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'" x-text="destPathError"></p>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-secondary h-10 whitespace-nowrap"
|
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||||
@click="checkPath(destinationPath, 'dest')"
|
@click="checkPath(destinationPath, 'dest')"
|
||||||
>
|
>
|
||||||
<i class="fas fa-check-circle mr-2"></i>
|
<svg class="w-4 h-4 mr-2 inline" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||||
Check Location
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 10a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm-6.5 4.5A5.5 5.5 0 0 1 9 9h2a5.5 5.5 0 0 1 5.5 5.5V17a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1v-2.5Z"/>
|
||||||
</button>
|
</svg>
|
||||||
|
Check Location
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +1,113 @@
|
|||||||
package destination
|
package destination
|
||||||
|
|
||||||
templ MinIODestinationForm() {
|
templ MinIODestinationForm() {
|
||||||
<div class="sm:col-span-6 space-y-6">
|
<div class="space-y-6 mt-4">
|
||||||
<div class="sm:col-span-4">
|
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
|
||||||
<label for="dest_endpoint" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Endpoint URL</label>
|
<div class="flex items-center">
|
||||||
<div class="relative">
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<span>Configure your MinIO server details below. You'll need your access key, secret key, and endpoint information.</span>
|
||||||
<i class="fas fa-server text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="dest_endpoint"
|
|
||||||
id="dest_endpoint"
|
|
||||||
x-model="destEndpoint"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="https://minio.example.com"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="dest_bucket" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Bucket Name</label>
|
<label for="dest_endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Endpoint URL</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-database text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destEndpoint" x-bind:required="requiresDestination"
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="dest_bucket"
|
placeholder="https://minio.example.com" />
|
||||||
id="dest_bucket"
|
|
||||||
x-model="destBucket"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="my-bucket"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Full URL to your MinIO server including protocol (http:// or https://)
|
||||||
<div class="sm:col-span-4">
|
|
||||||
<label for="destination_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Path Prefix</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-folder text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="destination_path"
|
|
||||||
id="destination_path"
|
|
||||||
x-model="destinationPath"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="path/prefix/"/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Optional. If specified, files will be uploaded to this path in the bucket.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="dest_access_key" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Access Key</label>
|
<label for="dest_bucket" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Bucket Name</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-archive text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destBucket" x-bind:required="requiresDestination"
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="dest_access_key"
|
placeholder="my-bucket" />
|
||||||
id="dest_access_key"
|
|
||||||
x-model="destAccessKey"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Name of your MinIO bucket (case-sensitive)
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
|
||||||
<label for="dest_secret_key" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Secret Key</label>
|
<div class="mb-6">
|
||||||
|
<label for="dest_region" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Region (Optional)</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-lock text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-globe-americas text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="dest_region" name="dest_region" x-model="destRegion"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="us-east-1" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Optional: Specify the region if your MinIO setup requires it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="dest_access_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Access Key</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="dest_access_key" name="dest_access_key" x-model="destAccessKey" x-bind:required="requiresDestination"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="minioadmin" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your MinIO Access Key for authentication
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="dest_secret_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Key</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="password" id="dest_secret_key" name="dest_secret_key" x-model="destSecretKey" x-bind:required="requiresDestination"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="minioadmin" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your MinIO Secret Key for authentication
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="destination_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Path in Bucket</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="path/to/files/" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Path prefix within the bucket (e.g., "backups/"). Leave empty to access the entire bucket.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
|
||||||
|
<div class="flex">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-2 flex-shrink-0"></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium">Configuration Note</h3>
|
||||||
|
<p class="mt-1">Make sure your MinIO server is properly configured with TLS if you're using HTTPS, and that the server is accessible from this machine.</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="dest_secret_key"
|
|
||||||
id="dest_secret_key"
|
|
||||||
x-model="destSecretKey"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
|
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="dest_secret_key" :value="destSecretKey"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1,56 +1,82 @@
|
|||||||
package destination
|
package destination
|
||||||
|
|
||||||
templ NextCloudDestinationForm() {
|
templ NextCloudDestinationForm() {
|
||||||
<div class="sm:col-span-6 space-y-6">
|
<div class="space-y-6 mt-4">
|
||||||
<div class="sm:col-span-4">
|
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
|
||||||
<label for="dest_host" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">NextCloud URL</label>
|
<div class="flex items-center">
|
||||||
<div class="relative">
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<span>Configure your NextCloud connection details below. You'll need your server URL, username, and password.</span>
|
||||||
<i class="fas fa-cloud text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="dest_host"
|
|
||||||
id="dest_host"
|
|
||||||
x-model="destHost"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="https://nextcloud.example.com"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="dest_user" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Username</label>
|
<label for="dest_endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">NextCloud URL</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-user text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-cloud text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destEndpoint" x-bind:required="requiresDestination"
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="dest_user"
|
placeholder="https://nextcloud.example.com" />
|
||||||
id="dest_user"
|
|
||||||
x-model="destUser"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Full URL to your NextCloud server including protocol (https://)
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="dest_password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Password</label>
|
<label for="dest_user" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="nextcloud_username" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your NextCloud username
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="dest_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="password" id="dest_password" name="dest_password" x-model="destPassword" x-bind:required="requiresDestination"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="NextCloud password" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your NextCloud account password
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="destination_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Remote Path</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath" x-bind:required="requiresDestination"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="remote.php/dav/files/username/path/to/files" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Path to your files in NextCloud. Usually starts with "remote.php/dav/files/username/"
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
|
||||||
|
<div class="flex">
|
||||||
|
<i class="fas fa-lightbulb mr-2 flex-shrink-0"></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium">Connection Tip</h3>
|
||||||
|
<p class="mt-1">If you have two-factor authentication enabled on your NextCloud account, you'll need to create an app password in your NextCloud security settings.</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="dest_password"
|
|
||||||
id="dest_password"
|
|
||||||
x-model="destPassword"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="Password"/>
|
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="dest_password" :value="destPassword"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1,91 +1,97 @@
|
|||||||
package destination
|
package destination
|
||||||
|
|
||||||
templ S3DestinationForm() {
|
templ S3DestinationForm() {
|
||||||
<div class="sm:col-span-6 space-y-6">
|
<div class="space-y-6 mt-4">
|
||||||
<div class="sm:col-span-4">
|
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
|
||||||
<label for="dest_bucket" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Bucket Name</label>
|
<div class="flex items-center">
|
||||||
<div class="relative">
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<span>Configure your AWS S3 bucket details below. You'll need your access key, secret key, and bucket information.</span>
|
||||||
<i class="fas fa-database text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="dest_bucket"
|
|
||||||
id="dest_bucket"
|
|
||||||
x-model="destBucket"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="my-bucket"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="dest_region" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">AWS Region</label>
|
<label for="dest_region" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">AWS Region</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-globe-americas text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-globe text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="dest_region" name="dest_region" x-model="destRegion" x-bind:required="requiresDestination"
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="dest_region"
|
placeholder="us-east-1" />
|
||||||
id="dest_region"
|
|
||||||
x-model="destRegion"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="us-east-1"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
AWS Region where your S3 bucket is located (e.g., us-east-1, eu-west-1)
|
||||||
<div class="sm:col-span-4">
|
|
||||||
<label for="destination_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">S3 Path Prefix</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-folder text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="destination_path"
|
|
||||||
id="destination_path"
|
|
||||||
x-model="destinationPath"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="optional/path/prefix/"/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Optional. If specified, files will be uploaded to this path in the bucket.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="dest_access_key" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Access Key</label>
|
<label for="dest_bucket" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Bucket Name</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-archive text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destBucket" x-bind:required="requiresDestination"
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="dest_access_key"
|
placeholder="my-bucket" />
|
||||||
id="dest_access_key"
|
|
||||||
x-model="destAccessKey"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Name of your S3 bucket (case-sensitive)
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="dest_secret_key" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Secret Key</label>
|
<label for="dest_access_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Access Key ID</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-lock text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="dest_access_key" name="dest_access_key" x-model="destAccessKey" x-bind:required="requiresDestination"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="AKIAIOSFODNN7EXAMPLE" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your AWS Access Key ID
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="dest_secret_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Access Key</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="password" id="dest_secret_key" name="dest_secret_key" x-model="destSecretKey" x-bind:required="requiresDestination"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your AWS Secret Access Key
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="destination_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Path in Bucket</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="path/to/files/" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Path prefix within the bucket (e.g., "backups/"). Leave empty to access the entire bucket.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
|
||||||
|
<div class="flex">
|
||||||
|
<i class="fas fa-shield-alt mr-2 flex-shrink-0"></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium">Security Note</h3>
|
||||||
|
<p class="mt-1">It's recommended to use an IAM user with restricted permissions for this configuration. The IAM user should only have access to the specific S3 bucket and operations needed.</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="dest_secret_key"
|
|
||||||
id="dest_secret_key"
|
|
||||||
x-model="destSecretKey"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
|
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="dest_secret_key" :value="destSecretKey"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1,147 +1,97 @@
|
|||||||
package destination
|
package destination
|
||||||
|
|
||||||
templ SFTPDestinationForm() {
|
templ SFTPDestinationForm() {
|
||||||
<div class="sm:col-span-6 space-y-6">
|
<div class="space-y-6 mt-4">
|
||||||
<div class="sm:col-span-4">
|
<div>
|
||||||
<label for="dest_host" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Host</label>
|
<label for="dest_host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SFTP Host</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-server text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="dest_host" name="dest_host" x-model="destHost" x-bind:required="requiresDestination"
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="dest_host"
|
placeholder="sftp.example.com" />
|
||||||
id="dest_host"
|
|
||||||
x-model="destHost"
|
|
||||||
required
|
|
||||||
aria-describedby="dest_host_help"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="sftp.example.com"/>
|
|
||||||
</div>
|
</div>
|
||||||
<p id="dest_host_help" class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Hostname or IP address of the SFTP server</p>
|
||||||
Enter the SFTP server hostname or IP address.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div>
|
||||||
<label for="dest_port" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Port</label>
|
<label for="dest_port" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Port</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-plug text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-network-wired text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="number" id="dest_port" name="dest_port" x-model="destPort"
|
||||||
type="number"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="dest_port"
|
placeholder="22" min="1" max="65535" x-init="if (!destPort || destPort === 0) destPort = 22" />
|
||||||
id="dest_port"
|
|
||||||
x-model="destPort"
|
|
||||||
required
|
|
||||||
min="1"
|
|
||||||
max="65535"
|
|
||||||
value="22"
|
|
||||||
aria-describedby="dest_port_help"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="22"/>
|
|
||||||
</div>
|
</div>
|
||||||
<p id="dest_port_help" class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">SFTP port number (default: 22)</p>
|
||||||
Default SFTP port is 22.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div>
|
||||||
<label for="dest_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Remote Path</label>
|
<label for="dest_user" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-folder text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="destination_path"
|
placeholder="username" />
|
||||||
id="destination_path"
|
|
||||||
x-model="destinationPath"
|
|
||||||
required
|
|
||||||
aria-describedby="destination_path_help"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="/path/to/files"/>
|
|
||||||
</div>
|
</div>
|
||||||
<p id="destination_path_help" class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">SFTP username for authentication</p>
|
||||||
Absolute path to the files on the remote server.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div>
|
||||||
<label for="dest_user" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Username</label>
|
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Authentication Method</label>
|
||||||
<div class="relative">
|
<div class="flex items-center mb-4">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<input id="dest_auth_password" type="radio" value="password" name="dest_auth_type" x-model="destAuthType"
|
||||||
<i class="fas fa-user text-secondary-400 dark:text-secondary-600"></i>
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||||
</div>
|
<label for="dest_auth_password" class="ms-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="dest_user"
|
|
||||||
id="dest_user"
|
|
||||||
x-model="destUser"
|
|
||||||
required
|
|
||||||
aria-describedby="dest_user_help"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
|
|
||||||
</div>
|
</div>
|
||||||
<p id="dest_user_help" class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<div class="flex items-center">
|
||||||
Username for SFTP authentication.
|
<input id="dest_auth_key" type="radio" value="key" name="dest_auth_type" x-model="destAuthType"
|
||||||
</p>
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||||
</div>
|
<label for="dest_auth_key" class="ms-2 text-sm font-medium text-gray-900 dark:text-white">SSH Key</label>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
|
||||||
<label for="dest_auth_type" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Authentication Type</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-lock text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
id="dest_auth_type"
|
|
||||||
name="dest_auth_type"
|
|
||||||
x-model="destAuthType"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500">
|
|
||||||
<option value="password">Password</option>
|
|
||||||
<option value="key_file">SSH Key File</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4" x-show="destAuthType === 'password'">
|
<div x-show="destAuthType === 'password'">
|
||||||
<label for="dest_password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Password</label>
|
<label for="dest_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="password" id="dest_password" name="dest_password" x-model="destPassword"
|
||||||
type="password"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="dest_password"
|
placeholder="Enter password" x-bind:required="destAuthType === 'password'" />
|
||||||
id="dest_password"
|
|
||||||
x-model="destPassword"
|
|
||||||
x-bind:required="destAuthType === 'password'"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="Password"/>
|
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="dest_password" :value="destPassword"/>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">SFTP password for authentication</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4" x-show="destAuthType === 'key_file'">
|
<div x-show="destAuthType === 'key'">
|
||||||
<label for="dest_key_file" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Key File</label>
|
<label for="dest_key_file" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SSH Key File</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-file-alt text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="dest_key_file" name="dest_key_file" x-model="destKeyFile"
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="dest_key_file"
|
placeholder="/path/to/id_rsa" x-bind:required="destAuthType === 'key'" />
|
||||||
id="dest_key_file"
|
|
||||||
x-model="destKeyFile"
|
|
||||||
x-bind:required="destAuthType === 'key_file'"
|
|
||||||
aria-describedby="dest_key_file_help"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="/path/to/key"/>
|
|
||||||
</div>
|
</div>
|
||||||
<p id="dest_key_file_help" class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Path to the SSH private key file</p>
|
||||||
Absolute path to SSH private key file.
|
</div>
|
||||||
</p>
|
|
||||||
|
<div>
|
||||||
|
<label for="destination_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Remote Path</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath" x-bind:required="requiresDestination"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="/remote/path/to/files" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Full path to the directory on the SFTP server</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1,111 +1,112 @@
|
|||||||
package destination
|
package destination
|
||||||
|
|
||||||
templ SMBDestinationForm() {
|
templ SMBDestinationForm() {
|
||||||
<div class="sm:col-span-6 space-y-6">
|
<div class="space-y-6 mt-4">
|
||||||
<div class="sm:col-span-4">
|
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
|
||||||
<label for="dest_host" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Server</label>
|
<div class="flex items-center">
|
||||||
<div class="relative">
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<span>Configure your SMB/CIFS share connection details below. You'll need server address, share name, and credentials.</span>
|
||||||
<i class="fas fa-server text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="dest_host"
|
|
||||||
id="dest_host"
|
|
||||||
x-model="destHost"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="server.example.com"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="dest_share" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Share Name</label>
|
<label for="dest_host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Server Address</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-share-alt text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="dest_host" name="dest_host" x-model="destHost" x-bind:required="requiresDestination"
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="dest_share"
|
placeholder="192.168.1.100 or server.example.com" />
|
||||||
id="dest_share"
|
|
||||||
x-model="destShare"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="share_name"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
IP address or hostname of the SMB/CIFS server
|
||||||
<div class="sm:col-span-4">
|
|
||||||
<label for="destination_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Path</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-folder text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="destination_path"
|
|
||||||
id="destination_path"
|
|
||||||
x-model="destinationPath"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="path\to\destination"/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Optional. Subdirectory within the share.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="dest_domain" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Domain</label>
|
<label for="dest_share" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Share Name</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-building text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-share-alt text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="dest_share" name="dest_share" x-model="destShare" x-bind:required="requiresDestination"
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="dest_domain"
|
placeholder="Documents" />
|
||||||
id="dest_domain"
|
|
||||||
x-model="destDomain"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="DOMAIN"/>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Optional. Windows domain for authentication.
|
Name of the shared folder on the server (without leading slashes)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="dest_user" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Username</label>
|
<label for="dest_domain" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Domain (Optional)</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-user text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-sitemap text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="dest_domain" name="dest_domain" x-model="destDomain"
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="dest_user"
|
placeholder="WORKGROUP or DOMAIN" />
|
||||||
id="dest_user"
|
|
||||||
x-model="destUser"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Windows domain name (if applicable). Default is 'WORKGROUP' for standalone servers.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="dest_password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Password</label>
|
<label for="dest_user" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="smb_username" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Username for SMB/CIFS authentication
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="dest_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="password" id="dest_password" name="dest_password" x-model="destPassword" x-bind:required="requiresDestination"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="SMB password" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Password for SMB/CIFS authentication
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="destination_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Subfolder Path (Optional)</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="path/to/subfolder" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Path to a subfolder within the share (Optional). Use forward slashes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
|
||||||
|
<div class="flex">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-2 flex-shrink-0"></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium">Connection Note</h3>
|
||||||
|
<p class="mt-1">Make sure the SMB server is accessible from this machine and that the share permissions allow the specified user to read the files.</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="dest_password"
|
|
||||||
id="dest_password"
|
|
||||||
x-model="destPassword"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="Password"/>
|
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="dest_password" :value="destPassword"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1,75 +1,82 @@
|
|||||||
package destination
|
package destination
|
||||||
|
|
||||||
templ WebDAVDestinationForm() {
|
templ WebDAVDestinationForm() {
|
||||||
<div class="sm:col-span-6 space-y-6">
|
<div class="space-y-6 mt-4">
|
||||||
<div class="sm:col-span-4">
|
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
|
||||||
<label for="dest_host" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">WebDAV URL</label>
|
<div class="flex items-center">
|
||||||
<div class="relative">
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<span>Configure your WebDAV connection details below. You'll need the server URL, username, and password.</span>
|
||||||
<i class="fas fa-globe text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="dest_host"
|
|
||||||
id="dest_host"
|
|
||||||
x-model="destHost"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="https://webdav.example.com/remote.php/webdav/"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="destination_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Remote Path</label>
|
<label for="dest_endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">WebDAV URL</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-folder text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-globe text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destEndpoint" x-bind:required="requiresDestination"
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="destination_path"
|
placeholder="https://webdav.example.com" />
|
||||||
id="destination_path"
|
|
||||||
x-model="destinationPath"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="/path/to/destination"/>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Optional. Relative to the WebDAV URL.
|
Full URL to your WebDAV server including protocol (https://)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="dest_user" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Username</label>
|
<label for="dest_user" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-user text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="dest_user"
|
placeholder="webdav_username" />
|
||||||
id="dest_user"
|
|
||||||
x-model="destUser"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your WebDAV username
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="dest_password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Password</label>
|
<label for="dest_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="password" id="dest_password" name="dest_password" x-model="destPassword" x-bind:required="requiresDestination"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="WebDAV password" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your WebDAV account password
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="destination_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Remote Path</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="/path/to/files" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Path to your files on the WebDAV server
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
|
||||||
|
<div class="flex">
|
||||||
|
<i class="fas fa-lightbulb mr-2 flex-shrink-0"></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium">Integration Note</h3>
|
||||||
|
<p class="mt-1">WebDAV is supported by many services including ownCloud, Nextcloud, Box, and many others. Make sure your WebDAV URL includes the full path to the WebDAV interface.</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="dest_password"
|
|
||||||
id="dest_password"
|
|
||||||
x-model="destPassword"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="Password"/>
|
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="dest_password" :value="destPassword"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ templ ProviderForm(formType string, providers []string, isSource bool) {
|
|||||||
id={fmt.Sprintf("%s_provider", formType)}
|
id={fmt.Sprintf("%s_provider", formType)}
|
||||||
name={fmt.Sprintf("%s_provider", formType)}
|
name={fmt.Sprintf("%s_provider", formType)}
|
||||||
x-model={fmt.Sprintf("%sProvider", formType)}
|
x-model={fmt.Sprintf("%sProvider", formType)}
|
||||||
class="form-select pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500">
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
<option value="" disabled selected>Select provider type</option>
|
<option value="" disabled selected>Select provider type</option>
|
||||||
for _, provider := range providers {
|
for _, provider := range providers {
|
||||||
<option value={provider}>{providerDisplayName(provider)}</option>
|
<option value={provider}>{providerDisplayName(provider)}</option>
|
||||||
|
|||||||
@@ -1,367 +0,0 @@
|
|||||||
package providers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/starfleetcptn/gomft/components/providers/common"
|
|
||||||
"github.com/starfleetcptn/gomft/components/providers/destination"
|
|
||||||
"github.com/starfleetcptn/gomft/components/providers/source"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Test that both source and destination providers can be rendered together with common components
|
|
||||||
func TestProvidersIntegration(t *testing.T) {
|
|
||||||
// Create context for test
|
|
||||||
ctx := context.Background()
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
// Test rendering common components
|
|
||||||
{
|
|
||||||
var buf strings.Builder
|
|
||||||
err := common.NameField().Render(ctx, &buf)
|
|
||||||
assert.NoError(err, "Failed to render NameField")
|
|
||||||
html := buf.String()
|
|
||||||
assert.Contains(html, `<label for="name"`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test rendering source components
|
|
||||||
{
|
|
||||||
var buf strings.Builder
|
|
||||||
err := source.LocalSourceForm().Render(ctx, &buf)
|
|
||||||
assert.NoError(err, "Failed to render LocalSourceForm")
|
|
||||||
html := buf.String()
|
|
||||||
assert.Contains(html, `<label for="source_path"`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test rendering destination components
|
|
||||||
{
|
|
||||||
var buf strings.Builder
|
|
||||||
err := destination.LocalDestinationForm().Render(ctx, &buf)
|
|
||||||
assert.NoError(err, "Failed to render LocalDestinationForm")
|
|
||||||
html := buf.String()
|
|
||||||
assert.Contains(html, `<label for="destination_path"`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that all source providers are available
|
|
||||||
func TestSourceProviders(t *testing.T) {
|
|
||||||
// Create context for test
|
|
||||||
ctx := context.Background()
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
// Test each source provider can be rendered
|
|
||||||
providers := []struct {
|
|
||||||
name string
|
|
||||||
template func() (string, error)
|
|
||||||
}{
|
|
||||||
{"LocalSourceForm", func() (string, error) {
|
|
||||||
var buf strings.Builder
|
|
||||||
err := source.LocalSourceForm().Render(ctx, &buf)
|
|
||||||
return buf.String(), err
|
|
||||||
}},
|
|
||||||
{"SFTPSourceForm", func() (string, error) {
|
|
||||||
var buf strings.Builder
|
|
||||||
err := source.SFTPSourceForm().Render(ctx, &buf)
|
|
||||||
return buf.String(), err
|
|
||||||
}},
|
|
||||||
{"S3SourceForm", func() (string, error) {
|
|
||||||
var buf strings.Builder
|
|
||||||
err := source.S3SourceForm().Render(ctx, &buf)
|
|
||||||
return buf.String(), err
|
|
||||||
}},
|
|
||||||
{"FTPSourceForm", func() (string, error) {
|
|
||||||
var buf strings.Builder
|
|
||||||
err := source.FTPSourceForm().Render(ctx, &buf)
|
|
||||||
return buf.String(), err
|
|
||||||
}},
|
|
||||||
{"SMBSourceForm", func() (string, error) {
|
|
||||||
var buf strings.Builder
|
|
||||||
err := source.SMBSourceForm().Render(ctx, &buf)
|
|
||||||
return buf.String(), err
|
|
||||||
}},
|
|
||||||
{"WebDAVSourceForm", func() (string, error) {
|
|
||||||
var buf strings.Builder
|
|
||||||
err := source.WebDAVSourceForm().Render(ctx, &buf)
|
|
||||||
return buf.String(), err
|
|
||||||
}},
|
|
||||||
{"GoogleDriveSourceForm", func() (string, error) {
|
|
||||||
var buf strings.Builder
|
|
||||||
err := source.GoogleDriveSourceForm().Render(ctx, &buf)
|
|
||||||
return buf.String(), err
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, provider := range providers {
|
|
||||||
t.Run(provider.name, func(t *testing.T) {
|
|
||||||
html, err := provider.template()
|
|
||||||
assert.NoError(err, "Failed to render "+provider.name)
|
|
||||||
assert.NotEmpty(html, provider.name+" rendered empty HTML")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that all destination providers are available
|
|
||||||
func TestDestinationProviders(t *testing.T) {
|
|
||||||
// Create context for test
|
|
||||||
ctx := context.Background()
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
// Test each destination provider can be rendered
|
|
||||||
providers := []struct {
|
|
||||||
name string
|
|
||||||
template func() (string, error)
|
|
||||||
}{
|
|
||||||
{"LocalDestinationForm", func() (string, error) {
|
|
||||||
var buf strings.Builder
|
|
||||||
err := destination.LocalDestinationForm().Render(ctx, &buf)
|
|
||||||
return buf.String(), err
|
|
||||||
}},
|
|
||||||
{"SFTPDestinationForm", func() (string, error) {
|
|
||||||
var buf strings.Builder
|
|
||||||
err := destination.SFTPDestinationForm().Render(ctx, &buf)
|
|
||||||
return buf.String(), err
|
|
||||||
}},
|
|
||||||
{"S3DestinationForm", func() (string, error) {
|
|
||||||
var buf strings.Builder
|
|
||||||
err := destination.S3DestinationForm().Render(ctx, &buf)
|
|
||||||
return buf.String(), err
|
|
||||||
}},
|
|
||||||
{"FTPDestinationForm", func() (string, error) {
|
|
||||||
var buf strings.Builder
|
|
||||||
err := destination.FTPDestinationForm().Render(ctx, &buf)
|
|
||||||
return buf.String(), err
|
|
||||||
}},
|
|
||||||
{"SMBDestinationForm", func() (string, error) {
|
|
||||||
var buf strings.Builder
|
|
||||||
err := destination.SMBDestinationForm().Render(ctx, &buf)
|
|
||||||
return buf.String(), err
|
|
||||||
}},
|
|
||||||
{"WebDAVDestinationForm", func() (string, error) {
|
|
||||||
var buf strings.Builder
|
|
||||||
err := destination.WebDAVDestinationForm().Render(ctx, &buf)
|
|
||||||
return buf.String(), err
|
|
||||||
}},
|
|
||||||
{"GoogleDriveDestinationForm", func() (string, error) {
|
|
||||||
var buf strings.Builder
|
|
||||||
err := destination.GoogleDriveDestinationForm().Render(ctx, &buf)
|
|
||||||
return buf.String(), err
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, provider := range providers {
|
|
||||||
t.Run(provider.name, func(t *testing.T) {
|
|
||||||
html, err := provider.template()
|
|
||||||
assert.NoError(err, "Failed to render "+provider.name)
|
|
||||||
assert.NotEmpty(html, provider.name+" rendered empty HTML")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test the complete configuration wizard flow
|
|
||||||
func TestConfigurationWizard(t *testing.T) {
|
|
||||||
// Create context for test
|
|
||||||
ctx := context.Background()
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
// First test common configuration fields
|
|
||||||
var buf strings.Builder
|
|
||||||
err := common.NameField().Render(ctx, &buf)
|
|
||||||
assert.NoError(err, "Failed to render name field")
|
|
||||||
nameField := buf.String()
|
|
||||||
assert.Contains(nameField, `<input type="text" name="name" id="name"`)
|
|
||||||
|
|
||||||
// Test source selection
|
|
||||||
buf.Reset()
|
|
||||||
err = common.SourceSelection().Render(ctx, &buf)
|
|
||||||
assert.NoError(err, "Failed to render source selection")
|
|
||||||
sourceSelection := buf.String()
|
|
||||||
assert.Contains(sourceSelection, `<select id="source_type" name="source_type"`)
|
|
||||||
|
|
||||||
// Test specific source form (local example)
|
|
||||||
buf.Reset()
|
|
||||||
err = source.LocalSourceForm().Render(ctx, &buf)
|
|
||||||
assert.NoError(err, "Failed to render local source form")
|
|
||||||
localSource := buf.String()
|
|
||||||
assert.Contains(localSource, `<input type="text" name="source_path" id="source_path"`)
|
|
||||||
|
|
||||||
// Test destination selection
|
|
||||||
buf.Reset()
|
|
||||||
err = common.DestinationSelection().Render(ctx, &buf)
|
|
||||||
assert.NoError(err, "Failed to render destination selection")
|
|
||||||
destinationSelection := buf.String()
|
|
||||||
assert.Contains(destinationSelection, `<select id="destination_type" name="destination_type"`)
|
|
||||||
|
|
||||||
// Test specific destination form (S3 example)
|
|
||||||
buf.Reset()
|
|
||||||
err = destination.S3DestinationForm().Render(ctx, &buf)
|
|
||||||
assert.NoError(err, "Failed to render S3 destination form")
|
|
||||||
s3Destination := buf.String()
|
|
||||||
assert.Contains(s3Destination, `<input type="text" name="dest_bucket" id="dest_bucket"`)
|
|
||||||
|
|
||||||
// Test advanced options
|
|
||||||
buf.Reset()
|
|
||||||
err = common.ArchiveOptions().Render(ctx, &buf)
|
|
||||||
assert.NoError(err, "Failed to render archive options")
|
|
||||||
archiveOptions := buf.String()
|
|
||||||
assert.Contains(archiveOptions, `Enable archiving`)
|
|
||||||
|
|
||||||
buf.Reset()
|
|
||||||
err = common.FilePatternFields().Render(ctx, &buf)
|
|
||||||
assert.NoError(err, "Failed to render file pattern fields")
|
|
||||||
filePatterns := buf.String()
|
|
||||||
assert.Contains(filePatterns, `<input type="text" name="file_pattern" id="file_pattern"`)
|
|
||||||
|
|
||||||
// All essential components for the configuration wizard are present and renderable
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that provider forms have proper conditional logic
|
|
||||||
func TestProviderFormConditionals(t *testing.T) {
|
|
||||||
// Create context for test
|
|
||||||
ctx := context.Background()
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
// Test SFTP Source form conditionals (password vs key file)
|
|
||||||
{
|
|
||||||
var buf strings.Builder
|
|
||||||
err := source.SFTPSourceForm().Render(ctx, &buf)
|
|
||||||
assert.NoError(err, "Failed to render SFTP source form")
|
|
||||||
html := buf.String()
|
|
||||||
|
|
||||||
// Should have auth type selection
|
|
||||||
assert.Contains(html, `<select id="source_auth_type" name="source_auth_type"`)
|
|
||||||
|
|
||||||
// Should have password field that's conditionally shown
|
|
||||||
assert.Contains(html, `x-show="sourceAuthType === 'password'"`)
|
|
||||||
assert.Contains(html, `<input type="password" name="source_password"`)
|
|
||||||
|
|
||||||
// Should have key file field that's conditionally shown
|
|
||||||
assert.Contains(html, `x-show="sourceAuthType === 'key_file'"`)
|
|
||||||
assert.Contains(html, `<input type="text" name="source_key_file"`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test S3 Source form conditionals
|
|
||||||
{
|
|
||||||
var buf strings.Builder
|
|
||||||
err := source.S3SourceForm().Render(ctx, &buf)
|
|
||||||
assert.NoError(err, "Failed to render S3 source form")
|
|
||||||
html := buf.String()
|
|
||||||
|
|
||||||
// Should have both required and optional fields
|
|
||||||
assert.Contains(html, `<input type="text" name="source_bucket" id="source_bucket" x-model="sourceBucket" required`)
|
|
||||||
assert.Contains(html, `<input type="text" name="source_region" id="source_region"`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test advanced options show/hide behavior
|
|
||||||
{
|
|
||||||
var buf strings.Builder
|
|
||||||
err := common.ArchiveOptions().Render(ctx, &buf)
|
|
||||||
assert.NoError(err, "Failed to render archive options")
|
|
||||||
html := buf.String()
|
|
||||||
|
|
||||||
// Archive path should only show when archive is enabled
|
|
||||||
assert.Contains(html, `x-show="archiveEnabled"`)
|
|
||||||
assert.Contains(html, `<input id="archive_path" name="archive_path" type="text"`)
|
|
||||||
|
|
||||||
// Toggle behavior
|
|
||||||
assert.Contains(html, `x-model="archiveEnabled"`)
|
|
||||||
assert.Contains(html, `<input id="archive_enabled" name="archive_enabled" type="checkbox"`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for accessibility attributes in provider forms
|
|
||||||
func TestProviderFormsAccessibility(t *testing.T) {
|
|
||||||
// Create context for test
|
|
||||||
ctx := context.Background()
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
// Test source form for accessibility
|
|
||||||
{
|
|
||||||
var buf strings.Builder
|
|
||||||
err := source.LocalSourceForm().Render(ctx, &buf)
|
|
||||||
assert.NoError(err, "Failed to render local source form")
|
|
||||||
html := buf.String()
|
|
||||||
|
|
||||||
// Should have labels with proper for attributes
|
|
||||||
assert.Contains(html, `<label for="source_path"`)
|
|
||||||
|
|
||||||
// Should have input with id matching label's for attribute
|
|
||||||
assert.Contains(html, `<input type="text" name="source_path" id="source_path"`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test destination form for accessibility
|
|
||||||
{
|
|
||||||
var buf strings.Builder
|
|
||||||
err := destination.LocalDestinationForm().Render(ctx, &buf)
|
|
||||||
assert.NoError(err, "Failed to render local destination form")
|
|
||||||
html := buf.String()
|
|
||||||
|
|
||||||
// Should have labels with proper for attributes
|
|
||||||
assert.Contains(html, `<label for="destination_path"`)
|
|
||||||
|
|
||||||
// Should have input with id matching label's for attribute
|
|
||||||
assert.Contains(html, `<input type="text" name="destination_path" id="destination_path"`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test dynamic form rendering based on provider selection
|
|
||||||
func TestDynamicFormRendering(t *testing.T) {
|
|
||||||
// Create context for test
|
|
||||||
ctx := context.Background()
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
// Test source selection dynamic rendering
|
|
||||||
{
|
|
||||||
var buf strings.Builder
|
|
||||||
err := common.SourceSelection().Render(ctx, &buf)
|
|
||||||
assert.NoError(err, "Failed to render source selection")
|
|
||||||
html := buf.String()
|
|
||||||
|
|
||||||
// Should have x-model for binding selected value
|
|
||||||
assert.Contains(html, `x-model="sourceType"`)
|
|
||||||
|
|
||||||
// The source selection component doesn't contain x-show attributes
|
|
||||||
// These assertions are removed as they're not part of the actual component
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test destination selection dynamic rendering
|
|
||||||
{
|
|
||||||
var buf strings.Builder
|
|
||||||
err := common.DestinationSelection().Render(ctx, &buf)
|
|
||||||
assert.NoError(err, "Failed to render destination selection")
|
|
||||||
html := buf.String()
|
|
||||||
|
|
||||||
// Should have x-model for binding selected value
|
|
||||||
assert.Contains(html, `x-model="destinationType"`)
|
|
||||||
|
|
||||||
// The destination selection component doesn't contain x-show attributes
|
|
||||||
// These assertions are removed as they're not part of the actual component
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for proper Alpine.js initialization
|
|
||||||
{
|
|
||||||
var buf strings.Builder
|
|
||||||
err := source.LocalSourceForm().Render(ctx, &buf)
|
|
||||||
assert.NoError(err)
|
|
||||||
html := buf.String()
|
|
||||||
|
|
||||||
// The LocalSourceForm doesn't initialize Alpine.js data
|
|
||||||
// It's expected to be used within a parent component that does
|
|
||||||
assert.Contains(html, `x-model="sourcePath"`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that wizard has a submission handler
|
|
||||||
{
|
|
||||||
var buf strings.Builder
|
|
||||||
// The source selection component doesn't contain form tags
|
|
||||||
// These assertions are checking for elements that should be in a parent component
|
|
||||||
err := common.SourceSelection().Render(ctx, &buf)
|
|
||||||
assert.NoError(err)
|
|
||||||
html := buf.String()
|
|
||||||
|
|
||||||
// Check for the select element instead
|
|
||||||
assert.Contains(html, `<select id="source_type" name="source_type"`)
|
|
||||||
assert.Contains(html, `x-model="sourceType"`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +1,79 @@
|
|||||||
package source
|
package source
|
||||||
|
|
||||||
templ FTPSourceForm() {
|
templ FTPSourceForm() {
|
||||||
<div class="sm:col-span-6 space-y-6">
|
<div class="space-y-6 mt-4">
|
||||||
<div class="sm:col-span-4">
|
<div>
|
||||||
<label for="source_host" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Host</label>
|
<label for="source_host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">FTP Host</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-server text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="source_host" name="source_host" x-model="sourceHost" required
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_host"
|
placeholder="ftp.example.com" />
|
||||||
id="source_host"
|
|
||||||
x-model="sourceHost"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="ftp.example.com"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Hostname or IP address of the FTP server</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div>
|
||||||
<label for="source_port" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Port</label>
|
<label for="source_port" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Port</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-plug text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-network-wired text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="number" id="source_port" name="source_port" x-model="sourcePort"
|
||||||
type="number"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_port"
|
placeholder="21" min="1" max="65535" value="21"/>
|
||||||
id="source_port"
|
|
||||||
x-model="sourcePort"
|
|
||||||
required
|
|
||||||
min="1"
|
|
||||||
max="65535"
|
|
||||||
value="21"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="21"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">FTP port number (default: 21)</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div>
|
||||||
<label for="source_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Remote Path</label>
|
<label for="source_user" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-folder text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="source_user" name="source_user" x-model="sourceUser" required
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_path"
|
placeholder="username" />
|
||||||
id="source_path"
|
|
||||||
x-model="sourcePath"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="/path/to/files"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">FTP username for authentication</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div>
|
||||||
<label for="source_user" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Username</label>
|
<label for="source_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-user text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="password" id="source_password" name="source_password" x-model="sourcePassword" required
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_user"
|
placeholder="Enter password" />
|
||||||
id="source_user"
|
|
||||||
x-model="sourceUser"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">FTP password for authentication</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="flex items-center mb-6">
|
||||||
<label for="source_password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Password</label>
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
<div class="relative">
|
<input type="checkbox" id="source_passive_mode" name="source_passive_mode" x-model="sourcePassiveMode"
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
class="sr-only peer" :value="sourcePassiveMode ? 'true' : 'false'">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||||
</div>
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-white">Use Passive Mode</span>
|
||||||
<input
|
</label>
|
||||||
type="password"
|
|
||||||
name="source_password"
|
|
||||||
id="source_password"
|
|
||||||
x-model="sourcePassword"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="Password"/>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" name="source_password" :value="sourcePassword"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-start mb-4">
|
<div>
|
||||||
<div class="flex items-center h-5">
|
<label for="source_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Remote Path</label>
|
||||||
<input
|
<div class="relative">
|
||||||
id="source_passive_mode"
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
name="source_passive_mode"
|
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||||
type="checkbox"
|
</div>
|
||||||
x-model="sourcePassiveMode"
|
<input type="text" id="source_path" name="source_path" x-model="sourcePath" required
|
||||||
:value="sourcePassiveMode ? 'true' : 'false'"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
class="focus:ring-primary-500 h-4 w-4 text-primary-600 border-secondary-300 dark:border-secondary-700 rounded">
|
placeholder="/remote/path/to/files" />
|
||||||
</div>
|
|
||||||
<div class="ml-3 text-sm">
|
|
||||||
<label for="source_passive_mode" class="font-medium text-secondary-700 dark:text-secondary-300">Use Passive Mode</label>
|
|
||||||
<p class="text-secondary-500 dark:text-secondary-400">Enable passive mode for FTP connection</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Full path to the directory on the FTP server</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1,175 +1,106 @@
|
|||||||
package source
|
package source
|
||||||
|
|
||||||
templ GoogleDriveSourceForm() {
|
templ GoogleDriveSourceForm() {
|
||||||
<div class="space-y-6" x-init="$watch('useBuiltinAuthSource', value => {
|
<div class="space-y-6 mt-4">
|
||||||
if(value) {
|
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
|
||||||
sourceClientId = '';
|
<div class="flex items-center">
|
||||||
sourceClientSecret = '';
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
}
|
<span>Google Drive requires authentication after creating this configuration. You'll be prompted to authenticate once the configuration is saved.</span>
|
||||||
})">
|
</div>
|
||||||
<div class="mb-6">
|
</div>
|
||||||
<label for="use_builtin_auth_source" class="flex items-center cursor-pointer">
|
|
||||||
<div class="relative">
|
|
||||||
<input id="use_builtin_auth_source" name="use_builtin_auth_source" type="checkbox"
|
|
||||||
class="sr-only"
|
|
||||||
x-model="useBuiltinAuthSource"
|
|
||||||
:value="useBuiltinAuthSource ? 'true' : 'false'"
|
|
||||||
/>
|
|
||||||
<div class="block bg-secondary-200 dark:bg-secondary-700 w-14 h-8 rounded-full"></div>
|
|
||||||
<div class="dot absolute left-1 top-1 bg-white dark:bg-secondary-100 w-6 h-6 rounded-full transition"
|
|
||||||
:class="useBuiltinAuthSource ? 'transform translate-x-6 bg-primary-500' : ''"></div>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 text-secondary-700 dark:text-secondary-300 font-medium">
|
|
||||||
Use rclone's built-in Google authentication (recommended)
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<p class="mt-1 ml-14 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Simple one-click authentication using rclone's shared credentials
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div x-bind:class="{ 'opacity-50': useBuiltinAuthSource }">
|
<div>
|
||||||
<div>
|
<div class="flex items-center mb-6">
|
||||||
<label for="source_client_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
Google Client ID
|
<input type="checkbox" id="use_builtin_auth_source" name="use_builtin_auth_source" x-model="useBuiltinAuthSource"
|
||||||
<span x-show="useBuiltinAuthSource" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
class="sr-only peer" :value="useBuiltinAuthSource ? 'true' : 'false'">
|
||||||
</label>
|
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||||
<div class="relative">
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-white">Use built-in authentication</span>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
</label>
|
||||||
<i class="fas fa-id-card text-secondary-400 dark:text-secondary-600"></i>
|
</div>
|
||||||
</div>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<input type="text" id="source_client_id" name="source_client_id" x-model="sourceClientId"
|
Toggle to use your own Google API credentials instead of the built-in ones.
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
</p>
|
||||||
x-bind:disabled="useBuiltinAuthSource"
|
</div>
|
||||||
placeholder="Google Drive OAuth Client ID" />
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Client ID from Google Cloud Console
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
<div x-show="!useBuiltinAuthSource">
|
||||||
<label for="source_client_secret" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
|
||||||
Google Client Secret
|
<div class="flex">
|
||||||
<span x-show="useBuiltinAuthSource" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
<i class="fas fa-exclamation-triangle mr-2 flex-shrink-0"></i>
|
||||||
</label>
|
<span>You'll need to create your own Google API project with Drive API enabled and create OAuth credentials. Learn how at <a href="https://developers.google.com/drive/api/quickstart/go" class="font-medium underline hover:text-yellow-700 dark:hover:text-yellow-200" target="_blank">Google API documentation</a>.</span>
|
||||||
<div class="relative">
|
</div>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
</div>
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input type="password" id="source_client_secret" name="source_client_secret" x-model="sourceClientSecret"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
x-bind:disabled="useBuiltinAuthSource"
|
|
||||||
placeholder="Google Drive OAuth Client Secret" />
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Client Secret from Google Cloud Console
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mb-6">
|
||||||
<label for="source_drive_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Folder ID (Optional)</label>
|
<label for="source_client_id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Client ID</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-folder text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-id-card text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" id="source_drive_id" name="source_drive_id" x-model="sourceDriveId"
|
<input type="text" id="source_client_id" name="source_client_id" x-model="sourceClientId"
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
placeholder="Google Drive Folder ID (optional)" />
|
placeholder="Your Google API Client ID" x-bind:required="!useBuiltinAuthSource" />
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">OAuth 2.0 Client ID from Google Cloud Console</p>
|
||||||
Folder ID to use as the root (leave empty for "My Drive")
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mb-6">
|
||||||
<label for="source_team_drive" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Shared/Team Drive ID (Optional)</label>
|
<label for="source_client_secret" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Client Secret</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-users text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" id="source_team_drive" name="source_team_drive" x-model="sourceTeamDrive"
|
<input type="password" id="source_client_secret" name="source_client_secret" x-model="sourceClientSecret"
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
placeholder="Shared/Team Drive ID (optional)" />
|
placeholder="Your Google API Client Secret" x-bind:required="!useBuiltinAuthSource" />
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">OAuth 2.0 Client Secret from Google Cloud Console</p>
|
||||||
ID of the Shared Drive / Team Drive to use
|
</div>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
<div>
|
||||||
<label for="source_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Path</label>
|
<label for="source_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Drive Path</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-folder-open text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fab fa-google-drive text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" id="source_path" name="source_path" x-model="sourcePath"
|
<input type="text" id="source_path" name="source_path" x-model="sourcePath"
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
placeholder="Path within Google Drive (e.g., /backup)" />
|
placeholder="/path/in/drive" />
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Path within the Drive from which files will be transferred
|
Path to the directory in your Google Drive. Leave empty for the root of your Drive.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 bg-amber-50 dark:bg-amber-900/50 rounded-lg border border-amber-100 dark:border-amber-700">
|
<div>
|
||||||
<div class="flex">
|
<label for="source_drive_id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Drive ID (Optional)</label>
|
||||||
<div class="flex-shrink-0">
|
<div class="relative">
|
||||||
<i class="fas fa-exclamation-triangle text-amber-500 dark:text-amber-400"></i>
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
</div>
|
<i class="fas fa-fingerprint text-gray-400 dark:text-gray-500"></i>
|
||||||
<div class="ml-3">
|
</div>
|
||||||
<h3 class="text-sm font-medium text-amber-800 dark:text-amber-200">Authentication Required</h3>
|
<input type="text" id="source_drive_id" name="source_drive_id" x-model="sourceDriveId"
|
||||||
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
<p>After saving this configuration, you'll need to authenticate with Google Drive:</p>
|
placeholder="Specific Google Drive ID" />
|
||||||
<ol class="list-decimal list-inside mt-2 space-y-1">
|
</div>
|
||||||
<li>Go to the configurations page</li>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<li>Click the "Authenticate" button for this configuration</li>
|
For accessing a shared drive. Leave empty to use your personal Google Drive.
|
||||||
<li>You'll be redirected to Google to grant access permissions</li>
|
</p>
|
||||||
<li>After granting permission, you'll be automatically redirected back to GoMFT</li>
|
</div>
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-4 bg-blue-50 dark:bg-blue-900/50 rounded-lg border border-blue-100 dark:border-blue-700">
|
<div>
|
||||||
<div class="flex">
|
<label for="source_team_drive" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Team Drive (Optional)</label>
|
||||||
<div class="flex-shrink-0">
|
<div class="relative">
|
||||||
<i class="fas fa-info-circle text-blue-400 dark:text-blue-300"></i>
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
</div>
|
<i class="fas fa-users text-gray-400 dark:text-gray-500"></i>
|
||||||
<div class="ml-3">
|
</div>
|
||||||
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">Authentication Information</h3>
|
<input type="text" id="source_team_drive" name="source_team_drive" x-model="sourceTeamDrive"
|
||||||
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
<template x-if="useBuiltinAuthSource">
|
placeholder="Team Drive ID" />
|
||||||
<div>
|
</div>
|
||||||
<p>You're using rclone's built-in authentication, which simplifies the setup process:</p>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<ul class="list-disc list-inside mt-1 space-y-1">
|
For accessing a shared team drive. Leave empty for personal drive.
|
||||||
<li>No need to create your own Google API credentials</li>
|
</p>
|
||||||
<li>Authentication happens through Google's secure OAuth flow</li>
|
</div>
|
||||||
<li>Automatic redirection back to GoMFT after authentication</li>
|
</div>
|
||||||
</ul>
|
|
||||||
<p class="mt-2 text-xs text-amber-600 dark:text-amber-400">
|
|
||||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
|
||||||
Note: The built-in authentication uses shared credentials which have rate limits across all rclone users.
|
|
||||||
If you plan to transfer large amounts of data or run many concurrent transfers, consider creating your own credentials.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template x-if="!useBuiltinAuthSource">
|
|
||||||
<div>
|
|
||||||
<p>To use Google Drive with your own credentials:</p>
|
|
||||||
<ol class="list-decimal list-inside mt-1 space-y-1">
|
|
||||||
<li>Go to the <a href="https://console.cloud.google.com/" target="_blank" class="text-blue-600 dark:text-blue-400 underline">Google Cloud Console</a></li>
|
|
||||||
<li>Create a project and enable the Google Drive API</li>
|
|
||||||
<li>Create OAuth 2.0 credentials (Client ID & Secret)</li>
|
|
||||||
<li>Set authorized redirect URI to <code class="bg-blue-100 dark:bg-blue-800 px-1 py-0.5 rounded">http://localhost:53682/</code></li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
@@ -1,216 +1,188 @@
|
|||||||
package source
|
package source
|
||||||
|
|
||||||
templ GooglePhotosSourceForm() {
|
templ GooglePhotosSourceForm() {
|
||||||
<div class="space-y-6" x-init="$watch('useBuiltinAuthSource', value => {
|
<div class="space-y-6 mt-4" x-init="$watch('useBuiltinAuthSource', value => {
|
||||||
if(value) {
|
if(value) {
|
||||||
sourceClientId = '';
|
sourceClientId = '';
|
||||||
sourceClientSecret = '';
|
sourceClientSecret = '';
|
||||||
}
|
}
|
||||||
})">
|
})">
|
||||||
<div class="mb-6">
|
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
|
||||||
<label for="use_builtin_auth_source" class="flex items-center cursor-pointer">
|
<div class="flex items-center">
|
||||||
<div class="relative">
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
<input id="use_builtin_auth_source" name="use_builtin_auth_source" type="checkbox"
|
<span>Google Photos requires authentication after creating this configuration. You'll be prompted to authenticate once the configuration is saved.</span>
|
||||||
class="sr-only"
|
</div>
|
||||||
x-model="useBuiltinAuthSource"
|
</div>
|
||||||
:value="useBuiltinAuthSource ? 'true' : 'false'"
|
|
||||||
/>
|
|
||||||
<div class="block bg-secondary-200 dark:bg-secondary-700 w-14 h-8 rounded-full"></div>
|
|
||||||
<div class="dot absolute left-1 top-1 bg-white dark:bg-secondary-100 w-6 h-6 rounded-full transition"
|
|
||||||
:class="useBuiltinAuthSource ? 'transform translate-x-6 bg-primary-500' : ''"></div>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 text-secondary-700 dark:text-secondary-300 font-medium">
|
|
||||||
Use rclone's built-in Google authentication (recommended)
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<p class="mt-1 ml-14 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Simple one-click authentication using rclone's shared credentials
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div x-bind:class="{ 'opacity-50': useBuiltinAuthSource }">
|
<div>
|
||||||
<div>
|
<div class="flex items-center mb-6">
|
||||||
<label for="source_client_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
Google Client ID
|
<input type="checkbox" id="use_builtin_auth_source" name="use_builtin_auth_source" x-model="useBuiltinAuthSource"
|
||||||
<span x-show="useBuiltinAuthSource" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
class="sr-only peer" :value="useBuiltinAuthSource ? 'true' : 'false'">
|
||||||
</label>
|
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||||
<div class="relative">
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-white">Use rclone's built-in Google authentication (recommended)</span>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
</label>
|
||||||
<i class="fas fa-id-card text-secondary-400 dark:text-secondary-600"></i>
|
</div>
|
||||||
</div>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<input type="text" id="source_client_id" name="source_client_id" x-model="sourceClientId"
|
Simple one-click authentication using rclone's shared credentials
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
</p>
|
||||||
x-bind:disabled="useBuiltinAuthSource"
|
</div>
|
||||||
placeholder="Google Photos OAuth Client ID" />
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Client ID from Google Cloud Console
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
<div x-bind:class="{ 'opacity-50': useBuiltinAuthSource }">
|
||||||
<label for="source_client_secret" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<div class="mb-6">
|
||||||
Google Client Secret
|
<label for="source_client_id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<span x-show="useBuiltinAuthSource" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
Google Client ID
|
||||||
</label>
|
<span x-show="useBuiltinAuthSource" class="text-gray-500 dark:text-gray-400 text-xs font-normal">(Using rclone default)</span>
|
||||||
<div class="relative">
|
</label>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="relative">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
</div>
|
<i class="fas fa-id-card text-gray-400 dark:text-gray-500"></i>
|
||||||
<input type="password" id="source_client_secret" name="source_client_secret" x-model="sourceClientSecret"
|
</div>
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
<input type="text" id="source_client_id" name="source_client_id" x-model="sourceClientId"
|
||||||
x-bind:disabled="useBuiltinAuthSource"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
placeholder="Google Photos OAuth Client Secret" />
|
x-bind:disabled="useBuiltinAuthSource"
|
||||||
</div>
|
placeholder="Google Photos OAuth Client ID" />
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
</div>
|
||||||
Client Secret from Google Cloud Console
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
</p>
|
Client ID from Google Cloud Console
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mb-6">
|
||||||
<label for="source_read_only" class="flex items-center cursor-pointer">
|
<label for="source_client_secret" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<div class="relative">
|
Google Client Secret
|
||||||
<input id="source_read_only" name="source_read_only" type="checkbox"
|
<span x-show="useBuiltinAuthSource" class="text-gray-500 dark:text-gray-400 text-xs font-normal">(Using rclone default)</span>
|
||||||
class="sr-only"
|
</label>
|
||||||
x-model="sourceReadOnly"
|
<div class="relative">
|
||||||
:value="sourceReadOnly ? 'true' : 'false'"
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
/>
|
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
|
||||||
<div class="block bg-secondary-200 dark:bg-secondary-700 w-14 h-8 rounded-full"></div>
|
</div>
|
||||||
<div class="dot absolute left-1 top-1 bg-white dark:bg-secondary-100 w-6 h-6 rounded-full transition"
|
<input type="password" id="source_client_secret" name="source_client_secret" x-model="sourceClientSecret"
|
||||||
:class="sourceReadOnly ? 'transform translate-x-6 bg-primary-500' : ''"></div>
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
</div>
|
x-bind:disabled="useBuiltinAuthSource"
|
||||||
<div class="ml-3 text-secondary-700 dark:text-secondary-300 font-medium">
|
placeholder="Google Photos OAuth Client Secret" />
|
||||||
Read-only mode
|
</div>
|
||||||
</div>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
</label>
|
Client Secret from Google Cloud Console
|
||||||
<p class="mt-1 ml-14 text-xs text-secondary-500 dark:text-secondary-400">
|
</p>
|
||||||
Only request read-only access to your photos
|
</div>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="flex items-center mb-6">
|
||||||
<label for="source_start_year" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Start Year (Optional)</label>
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
<div class="relative">
|
<input type="checkbox" id="source_read_only" name="source_read_only" x-model="sourceReadOnly"
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
class="sr-only peer" :value="sourceReadOnly ? 'true' : 'false'">
|
||||||
<i class="fas fa-calendar text-secondary-400 dark:text-secondary-600"></i>
|
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||||
</div>
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-white">Read-only mode</span>
|
||||||
<input type="number" id="source_start_year" name="source_start_year" x-model="sourceStartYear"
|
</label>
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
</div>
|
||||||
placeholder="Only include photos after this year (default: 2000)" min="1900" max="2100" />
|
<p class="ms-14 text-sm text-gray-500 dark:text-gray-400">
|
||||||
</div>
|
Only request read-only access to your photos
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
</p>
|
||||||
Only include photos uploaded after this year
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mb-6">
|
||||||
<label for="source_include_archived" class="flex items-center cursor-pointer">
|
<label for="source_start_year" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Start Year (Optional)</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input id="source_include_archived" name="source_include_archived" type="checkbox"
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
class="sr-only"
|
<i class="fas fa-calendar text-gray-400 dark:text-gray-500"></i>
|
||||||
x-model="sourceIncludeArchived"
|
</div>
|
||||||
:value="sourceIncludeArchived ? 'true' : 'false'"
|
<input type="number" id="source_start_year" name="source_start_year" x-model="sourceStartYear"
|
||||||
/>
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
<div class="block bg-secondary-200 dark:bg-secondary-700 w-14 h-8 rounded-full"></div>
|
placeholder="Only include photos after this year (default: 2000)" min="1900" max="2100" />
|
||||||
<div class="dot absolute left-1 top-1 bg-white dark:bg-secondary-100 w-6 h-6 rounded-full transition"
|
</div>
|
||||||
:class="sourceIncludeArchived ? 'transform translate-x-6 bg-primary-500' : ''"></div>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
</div>
|
Only include photos uploaded after this year
|
||||||
<div class="ml-3 text-secondary-700 dark:text-secondary-300 font-medium">
|
</p>
|
||||||
Include archived media
|
</div>
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<p class="mt-1 ml-14 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Include archived photos and videos in media listings
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="flex items-center mb-6">
|
||||||
<label for="source_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Album Path</label>
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
<div class="relative">
|
<input type="checkbox" id="source_include_archived" name="source_include_archived" x-model="sourceIncludeArchived"
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
class="sr-only peer" :value="sourceIncludeArchived ? 'true' : 'false'">
|
||||||
<i class="fas fa-images text-secondary-400 dark:text-secondary-600"></i>
|
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||||
</div>
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-white">Include archived media</span>
|
||||||
<input type="text" id="source_path" name="source_path" x-model="sourcePath"
|
</label>
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
</div>
|
||||||
placeholder="Album path (e.g., album/my-photos)" />
|
<p class="ms-14 text-sm text-gray-500 dark:text-gray-400">
|
||||||
</div>
|
Include archived photos and videos in media listings
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
</p>
|
||||||
Path within Google Photos to download files from
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-4 bg-amber-50 dark:bg-amber-900/50 rounded-lg border border-amber-100 dark:border-amber-700">
|
<div class="mb-6">
|
||||||
<div class="flex">
|
<label for="source_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Album Path</label>
|
||||||
<div class="flex-shrink-0">
|
<div class="relative">
|
||||||
<i class="fas fa-exclamation-triangle text-amber-500 dark:text-amber-400"></i>
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
</div>
|
<i class="fas fa-images text-gray-400 dark:text-gray-500"></i>
|
||||||
<div class="ml-3">
|
</div>
|
||||||
<h3 class="text-sm font-medium text-amber-800 dark:text-amber-200">Authentication Required</h3>
|
<input type="text" id="source_path" name="source_path" x-model="sourcePath"
|
||||||
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
<p>After saving this configuration, you'll need to authenticate with Google Photos:</p>
|
placeholder="Album path (e.g., album/my-photos)" />
|
||||||
<ol class="list-decimal list-inside mt-2 space-y-1">
|
</div>
|
||||||
<li>Go to the configurations page</li>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<li>Click the "Authenticate" button for this configuration</li>
|
Path within Google Photos to download files from
|
||||||
<li>You'll be redirected to Google to grant access permissions</li>
|
</p>
|
||||||
<li>After granting permission, you'll be automatically redirected back to GoMFT</li>
|
</div>
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-4 bg-blue-50 dark:bg-blue-900/50 rounded-lg border border-blue-100 dark:border-blue-700">
|
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-shrink-0">
|
<i class="fas fa-exclamation-triangle mr-2 flex-shrink-0"></i>
|
||||||
<i class="fas fa-info-circle text-blue-400 dark:text-blue-300"></i>
|
<div>
|
||||||
</div>
|
<h3 class="font-medium">Authentication Required</h3>
|
||||||
<div class="ml-3">
|
<p class="mt-1">After saving this configuration, you'll need to authenticate with Google Photos:</p>
|
||||||
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">Authentication Information</h3>
|
<ol class="list-decimal list-inside mt-2 space-y-1">
|
||||||
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
|
<li>Go to the configurations page</li>
|
||||||
<template x-if="useBuiltinAuthSource">
|
<li>Click the "Authenticate" button for this configuration</li>
|
||||||
<div>
|
<li>You'll be redirected to Google to grant access permissions</li>
|
||||||
<p>You're using rclone's built-in authentication, which simplifies the setup process:</p>
|
<li>After granting permission, you'll be automatically redirected back to GoMFT</li>
|
||||||
<ul class="list-disc list-inside mt-1 space-y-1">
|
</ol>
|
||||||
<li>No need to create your own Google API credentials</li>
|
</div>
|
||||||
<li>Authentication happens through Google's secure OAuth flow</li>
|
</div>
|
||||||
<li>Automatic redirection back to GoMFT after authentication</li>
|
</div>
|
||||||
</ul>
|
|
||||||
<p class="mt-2 text-xs text-amber-600 dark:text-amber-400">
|
|
||||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
|
||||||
Note: The built-in authentication uses shared credentials which have rate limits across all rclone users.
|
|
||||||
If you plan to transfer large amounts of data or run many concurrent transfers, consider creating your own credentials.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template x-if="!useBuiltinAuthSource">
|
|
||||||
<div>
|
|
||||||
<p>To use Google Photos with your own credentials:</p>
|
|
||||||
<ol class="list-decimal list-inside mt-1 space-y-1">
|
|
||||||
<li>Go to the <a href="https://console.cloud.google.com/" target="_blank" class="text-blue-600 dark:text-blue-400 underline">Google Cloud Console</a></li>
|
|
||||||
<li>Create a project and enable the Google Photos API</li>
|
|
||||||
<li>Create OAuth 2.0 credentials (Client ID & Secret)</li>
|
|
||||||
<li>Set authorized redirect URI to <code class="bg-blue-100 dark:bg-blue-800 px-1 py-0.5 rounded">http://localhost:53682/</code></li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-4 bg-yellow-50 dark:bg-yellow-900/50 rounded-lg border border-yellow-100 dark:border-yellow-700">
|
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-shrink-0">
|
<i class="fas fa-info-circle mr-2 flex-shrink-0"></i>
|
||||||
<i class="fas fa-exclamation-circle text-yellow-500 dark:text-yellow-400"></i>
|
<div>
|
||||||
</div>
|
<h3 class="font-medium">Authentication Information</h3>
|
||||||
<div class="ml-3">
|
<div class="mt-1">
|
||||||
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">Important Note About Google Photos</h3>
|
<template x-if="useBuiltinAuthSource">
|
||||||
<div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
|
<div>
|
||||||
<p>When downloading from Google Photos, be aware that some original metadata may not be preserved. Google Photos processes and may compress some images upon upload.</p>
|
<p>You're using rclone's built-in authentication, which simplifies the setup process:</p>
|
||||||
</div>
|
<ul class="list-disc list-inside mt-1 space-y-1">
|
||||||
</div>
|
<li>No need to create your own Google API credentials</li>
|
||||||
</div>
|
<li>Authentication happens through Google's secure OAuth flow</li>
|
||||||
</div>
|
<li>Automatic redirection back to GoMFT after authentication</li>
|
||||||
</div>
|
</ul>
|
||||||
|
<p class="mt-2 text-yellow-700 dark:text-yellow-300">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-1"></i>
|
||||||
|
Note: The built-in authentication uses shared credentials which have rate limits across all rclone users.
|
||||||
|
If you plan to transfer large amounts of data or run many concurrent transfers, consider creating your own credentials.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-if="!useBuiltinAuthSource">
|
||||||
|
<div>
|
||||||
|
<p>To use Google Photos with your own credentials:</p>
|
||||||
|
<ol class="list-decimal list-inside mt-1 space-y-1">
|
||||||
|
<li>Go to the <a href="https://console.cloud.google.com/" target="_blank" class="font-medium underline hover:text-blue-700 dark:hover:text-blue-300">Google Cloud Console</a></li>
|
||||||
|
<li>Create a project and enable the Google Photos API</li>
|
||||||
|
<li>Create OAuth 2.0 credentials (Client ID & Secret)</li>
|
||||||
|
<li>Set authorized redirect URI to <code class="bg-blue-100 dark:bg-blue-800 px-1.5 py-0.5 rounded text-xs">http://localhost:53682/</code></li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
|
||||||
|
<div class="flex">
|
||||||
|
<i class="fas fa-exclamation-circle mr-2 flex-shrink-0"></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium">Important Note About Google Photos</h3>
|
||||||
|
<p class="mt-1">When downloading from Google Photos, be aware that some original metadata may not be preserved. Google Photos processes and may compress some images upon upload.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1,39 +1,41 @@
|
|||||||
package source
|
package source
|
||||||
|
|
||||||
templ LocalSourceForm() {
|
templ LocalSourceForm() {
|
||||||
<div class="sm:col-span-6 space-y-6">
|
<div class="space-y-4 mt-4">
|
||||||
<div class="sm:col-span-4">
|
<div>
|
||||||
<label for="source_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Local Path</label>
|
<label for="source_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Local Path</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-folder-open text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="source_path" name="source_path" x-model="sourcePath" required
|
||||||
|
:class="{ 'border-red-500 dark:border-red-700': sourcePathValid === false, 'border-green-500 dark:border-green-700': sourcePathValid === true }"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="/path/to/files" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Full path to the local directory containing your files</p>
|
||||||
|
<div x-show="sourcePathValid === false" class="mt-2 text-sm text-red-600 dark:text-red-400">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-exclamation-circle mr-2"></i>
|
||||||
|
<span x-text="sourcePathError"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div x-show="sourcePathValid === true" class="mt-2 text-sm text-green-600 dark:text-green-400">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-check-circle mr-2"></i>
|
||||||
|
<span x-text="sourcePathError || 'Path is valid'"></span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="source_path"
|
|
||||||
id="source_path"
|
|
||||||
x-model="sourcePath"
|
|
||||||
required
|
|
||||||
:class="{ 'border-red-300 dark:border-red-700': sourcePathValid === false, 'border-green-300 dark:border-green-700': sourcePathValid === true }"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="Local directory path (e.g., /path/to/files)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Full path to the local directory containing your files
|
|
||||||
</p>
|
|
||||||
<!-- Path validation message -->
|
|
||||||
<template x-if="sourcePathValid !== null">
|
|
||||||
<p x-show="sourcePathError" class="mt-1 text-sm" :class="sourcePathValid ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'" x-text="sourcePathError"></p>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-secondary h-10 whitespace-nowrap"
|
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||||
@click="checkPath(sourcePath, 'source')"
|
@click="checkPath(sourcePath, 'source')"
|
||||||
>
|
>
|
||||||
<i class="fas fa-check-circle mr-2"></i>
|
<svg class="w-4 h-4 mr-2 inline" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||||
Check Location
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 10a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm-6.5 4.5A5.5 5.5 0 0 1 9 9h2a5.5 5.5 0 0 1 5.5 5.5V17a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1v-2.5Z"/>
|
||||||
</button>
|
</svg>
|
||||||
|
Check Location
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +1,113 @@
|
|||||||
package source
|
package source
|
||||||
|
|
||||||
templ MinIOSourceForm() {
|
templ MinIOSourceForm() {
|
||||||
<div class="sm:col-span-6 space-y-6">
|
<div class="space-y-6 mt-4">
|
||||||
<div class="sm:col-span-4">
|
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
|
||||||
<label for="source_endpoint" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Endpoint URL</label>
|
<div class="flex items-center">
|
||||||
<div class="relative">
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<span>Configure your MinIO server details below. You'll need your access key, secret key, and endpoint information.</span>
|
||||||
<i class="fas fa-server text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="source_endpoint"
|
|
||||||
id="source_endpoint"
|
|
||||||
x-model="sourceEndpoint"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="https://minio.example.com"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="source_bucket" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Bucket Name</label>
|
<label for="source_endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Endpoint URL</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-database text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="source_endpoint" name="source_endpoint" x-model="sourceEndpoint" required
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_bucket"
|
placeholder="https://minio.example.com" />
|
||||||
id="source_bucket"
|
|
||||||
x-model="sourceBucket"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="my-bucket"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Full URL to your MinIO server including protocol (http:// or https://)
|
||||||
<div class="sm:col-span-4">
|
|
||||||
<label for="source_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Path Prefix</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-folder text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="source_path"
|
|
||||||
id="source_path"
|
|
||||||
x-model="sourcePath"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="path/prefix/"/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Optional. If specified, only files in this path will be processed.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="source_access_key" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Access Key</label>
|
<label for="source_bucket" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Bucket Name</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-archive text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="source_bucket" name="source_bucket" x-model="sourceBucket" required
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_access_key"
|
placeholder="my-bucket" />
|
||||||
id="source_access_key"
|
|
||||||
x-model="sourceAccessKey"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Name of your MinIO bucket (case-sensitive)
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
|
||||||
<label for="source_secret_key" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Secret Key</label>
|
<div class="mb-6">
|
||||||
|
<label for="source_region" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Region (Optional)</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-lock text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-globe-americas text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="source_region" name="source_region" x-model="sourceRegion"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="us-east-1" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Optional: Specify the region if your MinIO setup requires it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="source_access_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Access Key</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="source_access_key" name="source_access_key" x-model="sourceAccessKey" required
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="minioadmin" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your MinIO Access Key for authentication
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="source_secret_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Key</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="password" id="source_secret_key" name="source_secret_key" x-model="sourceSecretKey" required
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="minioadmin" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your MinIO Secret Key for authentication
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="source_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Path in Bucket</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="source_path" name="source_path" x-model="sourcePath"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="path/to/files/" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Path prefix within the bucket (e.g., "backups/"). Leave empty to access the entire bucket.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
|
||||||
|
<div class="flex">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-2 flex-shrink-0"></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium">Configuration Note</h3>
|
||||||
|
<p class="mt-1">Make sure your MinIO server is properly configured with TLS if you're using HTTPS, and that the server is accessible from this machine.</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="source_secret_key"
|
|
||||||
id="source_secret_key"
|
|
||||||
x-model="sourceSecretKey"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
|
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="source_secret_key" :value="sourceSecretKey"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1,75 +1,82 @@
|
|||||||
package source
|
package source
|
||||||
|
|
||||||
templ NextCloudSourceForm() {
|
templ NextCloudSourceForm() {
|
||||||
<div class="sm:col-span-6 space-y-6">
|
<div class="space-y-6 mt-4">
|
||||||
<div class="sm:col-span-4">
|
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
|
||||||
<label for="source_host" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">NextCloud URL</label>
|
<div class="flex items-center">
|
||||||
<div class="relative">
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<span>Configure your NextCloud connection details below. You'll need your server URL, username, and password.</span>
|
||||||
<i class="fas fa-cloud text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="source_host"
|
|
||||||
id="source_host"
|
|
||||||
x-model="sourceHost"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="https://nextcloud.example.com"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="source_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Remote Path</label>
|
<label for="source_endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">NextCloud URL</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-folder text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-cloud text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="source_endpoint" name="source_endpoint" x-model="sourceEndpoint" required
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_path"
|
placeholder="https://nextcloud.example.com" />
|
||||||
id="source_path"
|
|
||||||
x-model="sourcePath"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="/remote.php/dav/files/username/path/to/files"/>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Optional. If left empty, root directory will be used.
|
Full URL to your NextCloud server including protocol (https://)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="source_user" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Username</label>
|
<label for="source_user" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-user text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="source_user" name="source_user" x-model="sourceUser" required
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_user"
|
placeholder="nextcloud_username" />
|
||||||
id="source_user"
|
|
||||||
x-model="sourceUser"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your NextCloud username
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="source_password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Password</label>
|
<label for="source_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="password" id="source_password" name="source_password" x-model="sourcePassword" required
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="NextCloud password" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your NextCloud account password
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="source_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Remote Path</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="source_path" name="source_path" x-model="sourcePath" required
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="remote.php/dav/files/username/path/to/files" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Path to your files in NextCloud. Usually starts with "remote.php/dav/files/username/"
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
|
||||||
|
<div class="flex">
|
||||||
|
<i class="fas fa-lightbulb mr-2 flex-shrink-0"></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium">Connection Tip</h3>
|
||||||
|
<p class="mt-1">If you have two-factor authentication enabled on your NextCloud account, you'll need to create an app password in your NextCloud security settings.</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="source_password"
|
|
||||||
id="source_password"
|
|
||||||
x-model="sourcePassword"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="Password"/>
|
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="source_password" :value="sourcePassword"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1,100 +1,96 @@
|
|||||||
package source
|
package source
|
||||||
|
|
||||||
templ S3SourceForm() {
|
templ S3SourceForm() {
|
||||||
<div class="sm:col-span-6 space-y-6">
|
<div class="space-y-6 mt-4">
|
||||||
<div class="sm:col-span-4">
|
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
|
||||||
<label for="source_bucket" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Bucket Name</label>
|
<div class="flex items-center">
|
||||||
<div class="relative">
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<span>Configure your AWS S3 bucket details below. You'll need your access key, secret key, and bucket information.</span>
|
||||||
<i class="fab fa-aws text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="source_bucket"
|
|
||||||
id="source_bucket"
|
|
||||||
x-model="sourceBucket"
|
|
||||||
required
|
|
||||||
aria-describedby="source_bucket_help"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="my-bucket"/>
|
|
||||||
</div>
|
</div>
|
||||||
<p id="source_bucket_help" class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
</div>
|
||||||
Enter your S3 bucket name.
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="source_region" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">AWS Region</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-globe text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="source_region" name="source_region" x-model="sourceRegion" required
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="us-east-1" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
AWS Region where your S3 bucket is located (e.g., us-east-1, eu-west-1)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="source_region" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">AWS Region</label>
|
<label for="source_bucket" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Bucket Name</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-globe text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-archive text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="source_bucket" name="source_bucket" x-model="sourceBucket" required
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_region"
|
placeholder="my-bucket" />
|
||||||
id="source_region"
|
|
||||||
x-model="sourceRegion"
|
|
||||||
required
|
|
||||||
aria-describedby="source_region_help"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="us-west-2"/>
|
|
||||||
</div>
|
</div>
|
||||||
<p id="source_region_help" class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
AWS region for the S3 bucket (e.g., us-west-2).
|
Name of your S3 bucket (case-sensitive)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="source_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Path Prefix</label>
|
<label for="source_access_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Access Key ID</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-folder text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="source_access_key" name="source_access_key" x-model="sourceAccessKey" required
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_path"
|
placeholder="AKIAIOSFODNN7EXAMPLE" />
|
||||||
id="source_path"
|
|
||||||
x-model="sourcePath"
|
|
||||||
aria-describedby="source_path_help"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="path/to/files/"/>
|
|
||||||
</div>
|
</div>
|
||||||
<p id="source_path_help" class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Optional path prefix within the bucket (e.g., 'path/to/files/').
|
Your AWS Access Key ID
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="source_access_key" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Access Key ID</label>
|
<label for="source_secret_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Access Key</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="password" id="source_secret_key" name="source_secret_key" x-model="sourceSecretKey" required
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_access_key"
|
placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" />
|
||||||
id="source_access_key"
|
|
||||||
x-model="sourceAccessKey"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="AKIAIOSFODNN7EXAMPLE"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your AWS Secret Access Key
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="source_secret_key" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Secret Access Key</label>
|
<label for="source_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Path in Bucket</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-lock text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="source_path" name="source_path" x-model="sourcePath"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="path/to/files/" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Path prefix within the bucket (e.g., "backups/"). Leave empty to access the entire bucket.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
|
||||||
|
<div class="flex">
|
||||||
|
<i class="fas fa-shield-alt mr-2 flex-shrink-0"></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium">Security Note</h3>
|
||||||
|
<p class="mt-1">It's recommended to use an IAM user with restricted permissions for this configuration. The IAM user should only have access to the specific S3 bucket and operations needed.</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="source_secret_key"
|
|
||||||
id="source_secret_key"
|
|
||||||
x-model="sourceSecretKey"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="Your secret access key"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,147 +1,97 @@
|
|||||||
package source
|
package source
|
||||||
|
|
||||||
templ SFTPSourceForm() {
|
templ SFTPSourceForm() {
|
||||||
<div class="sm:col-span-6 space-y-6">
|
<div class="space-y-6 mt-4">
|
||||||
<div class="sm:col-span-4">
|
<div>
|
||||||
<label for="source_host" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Host</label>
|
<label for="source_host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SFTP Host</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-server text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="source_host" name="source_host" x-model="sourceHost" required
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_host"
|
placeholder="sftp.example.com" />
|
||||||
id="source_host"
|
|
||||||
x-model="sourceHost"
|
|
||||||
required
|
|
||||||
aria-describedby="source_host_help"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="sftp.example.com"/>
|
|
||||||
</div>
|
</div>
|
||||||
<p id="source_host_help" class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Hostname or IP address of the SFTP server</p>
|
||||||
Enter the SFTP server hostname or IP address.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div>
|
||||||
<label for="source_port" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Port</label>
|
<label for="source_port" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Port</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-plug text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-network-wired text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="number" id="source_port" name="source_port" x-model="sourcePort"
|
||||||
type="number"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_port"
|
placeholder="22" min="1" max="65535" />
|
||||||
id="source_port"
|
|
||||||
x-model="sourcePort"
|
|
||||||
required
|
|
||||||
min="1"
|
|
||||||
max="65535"
|
|
||||||
value="22"
|
|
||||||
aria-describedby="source_port_help"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="22"/>
|
|
||||||
</div>
|
</div>
|
||||||
<p id="source_port_help" class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">SFTP port number (default: 22)</p>
|
||||||
Default SFTP port is 22.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div>
|
||||||
<label for="source_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Remote Path</label>
|
<label for="source_user" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-folder text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="source_user" name="source_user" x-model="sourceUser" required
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_path"
|
placeholder="username" />
|
||||||
id="source_path"
|
|
||||||
x-model="sourcePath"
|
|
||||||
required
|
|
||||||
aria-describedby="source_path_help"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="/path/to/files"/>
|
|
||||||
</div>
|
</div>
|
||||||
<p id="source_path_help" class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">SFTP username for authentication</p>
|
||||||
Absolute path to the files on the remote server.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div>
|
||||||
<label for="source_user" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Username</label>
|
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Authentication Method</label>
|
||||||
<div class="relative">
|
<div class="flex items-center mb-4">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<input id="source_auth_password" type="radio" value="password" name="source_auth_type" x-model="sourceAuthType"
|
||||||
<i class="fas fa-user text-secondary-400 dark:text-secondary-600"></i>
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||||
</div>
|
<label for="source_auth_password" class="ms-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="source_user"
|
|
||||||
id="source_user"
|
|
||||||
x-model="sourceUser"
|
|
||||||
required
|
|
||||||
aria-describedby="source_user_help"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
|
|
||||||
</div>
|
</div>
|
||||||
<p id="source_user_help" class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<div class="flex items-center">
|
||||||
Username for SFTP authentication.
|
<input id="source_auth_key" type="radio" value="key" name="source_auth_type" x-model="sourceAuthType"
|
||||||
</p>
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||||
</div>
|
<label for="source_auth_key" class="ms-2 text-sm font-medium text-gray-900 dark:text-white">SSH Key</label>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
|
||||||
<label for="source_auth_type" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Authentication Type</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-lock text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
id="source_auth_type"
|
|
||||||
name="source_auth_type"
|
|
||||||
x-model="sourceAuthType"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500">
|
|
||||||
<option value="password">Password</option>
|
|
||||||
<option value="key_file">SSH Key File</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4" x-show="sourceAuthType === 'password'">
|
<div x-show="sourceAuthType === 'password'">
|
||||||
<label for="source_password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Password</label>
|
<label for="source_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="password" id="source_password" name="source_password" x-model="sourcePassword"
|
||||||
type="password"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_password"
|
placeholder="Enter password" x-bind:required="sourceAuthType === 'password'" />
|
||||||
id="source_password"
|
|
||||||
x-model="sourcePassword"
|
|
||||||
x-bind:required="sourceAuthType === 'password'"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="Password"/>
|
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="source_password" :value="sourcePassword"/>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">SFTP password for authentication</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4" x-show="sourceAuthType === 'key_file'">
|
<div x-show="sourceAuthType === 'key'">
|
||||||
<label for="source_key_file" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Key File</label>
|
<label for="source_key_file" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SSH Key File</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-file-alt text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="source_key_file" name="source_key_file" x-model="sourceKeyFile"
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_key_file"
|
placeholder="/path/to/id_rsa" x-bind:required="sourceAuthType === 'key'" />
|
||||||
id="source_key_file"
|
|
||||||
x-model="sourceKeyFile"
|
|
||||||
x-bind:required="sourceAuthType === 'key_file'"
|
|
||||||
aria-describedby="source_key_file_help"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="/path/to/key"/>
|
|
||||||
</div>
|
</div>
|
||||||
<p id="source_key_file_help" class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Path to the SSH private key file</p>
|
||||||
Absolute path to SSH private key file.
|
</div>
|
||||||
</p>
|
|
||||||
|
<div>
|
||||||
|
<label for="source_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Remote Path</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="source_path" name="source_path" x-model="sourcePath" required
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="/remote/path/to/files" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Full path to the directory on the SFTP server</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1,111 +1,112 @@
|
|||||||
package source
|
package source
|
||||||
|
|
||||||
templ SMBSourceForm() {
|
templ SMBSourceForm() {
|
||||||
<div class="sm:col-span-6 space-y-6">
|
<div class="space-y-6 mt-4">
|
||||||
<div class="sm:col-span-4">
|
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
|
||||||
<label for="source_host" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Server</label>
|
<div class="flex items-center">
|
||||||
<div class="relative">
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<span>Configure your SMB/CIFS share connection details below. You'll need server address, share name, and credentials.</span>
|
||||||
<i class="fas fa-server text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="source_host"
|
|
||||||
id="source_host"
|
|
||||||
x-model="sourceHost"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="server.example.com"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="source_share" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Share Name</label>
|
<label for="source_host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Server Address</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-share-alt text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="source_host" name="source_host" x-model="sourceHost" required
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_share"
|
placeholder="192.168.1.100 or server.example.com" />
|
||||||
id="source_share"
|
|
||||||
x-model="sourceShare"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="share_name"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
IP address or hostname of the SMB/CIFS server
|
||||||
<div class="sm:col-span-4">
|
|
||||||
<label for="source_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Path</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-folder text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="source_path"
|
|
||||||
id="source_path"
|
|
||||||
x-model="sourcePath"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="path\to\files"/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Optional. Subdirectory within the share.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="source_domain" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Domain</label>
|
<label for="source_share" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Share Name</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-building text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-share-alt text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="source_share" name="source_share" x-model="sourceShare" required
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_domain"
|
placeholder="Documents" />
|
||||||
id="source_domain"
|
|
||||||
x-model="sourceDomain"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="DOMAIN"/>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Optional. Windows domain for authentication.
|
Name of the shared folder on the server (without leading slashes)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="source_user" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Username</label>
|
<label for="source_domain" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Domain (Optional)</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-user text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-sitemap text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="source_domain" name="source_domain" x-model="sourceDomain"
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_user"
|
placeholder="WORKGROUP or DOMAIN" />
|
||||||
id="source_user"
|
|
||||||
x-model="sourceUser"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Windows domain name (if applicable). Default is 'WORKGROUP' for standalone servers.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="source_password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Password</label>
|
<label for="source_user" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="source_user" name="source_user" x-model="sourceUser" required
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="smb_username" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Username for SMB/CIFS authentication
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="source_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="password" id="source_password" name="source_password" x-model="sourcePassword" required
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="SMB password" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Password for SMB/CIFS authentication
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="source_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Subfolder Path (Optional)</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="source_path" name="source_path" x-model="sourcePath"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="path/to/subfolder" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Path to a subfolder within the share (Optional). Use forward slashes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
|
||||||
|
<div class="flex">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-2 flex-shrink-0"></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium">Connection Note</h3>
|
||||||
|
<p class="mt-1">Make sure the SMB server is accessible from this machine and that the share permissions allow the specified user to read the files.</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="source_password"
|
|
||||||
id="source_password"
|
|
||||||
x-model="sourcePassword"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="Password"/>
|
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="source_password" :value="sourcePassword"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1,75 +1,82 @@
|
|||||||
package source
|
package source
|
||||||
|
|
||||||
templ WebDAVSourceForm() {
|
templ WebDAVSourceForm() {
|
||||||
<div class="sm:col-span-6 space-y-6">
|
<div class="space-y-6 mt-4">
|
||||||
<div class="sm:col-span-4">
|
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
|
||||||
<label for="source_host" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">WebDAV URL</label>
|
<div class="flex items-center">
|
||||||
<div class="relative">
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<span>Configure your WebDAV connection details below. You'll need the server URL, username, and password.</span>
|
||||||
<i class="fas fa-globe text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="source_host"
|
|
||||||
id="source_host"
|
|
||||||
x-model="sourceHost"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="https://webdav.example.com/remote.php/webdav/"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="source_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Remote Path</label>
|
<label for="source_endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">WebDAV URL</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-folder text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-globe text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="source_endpoint" name="source_endpoint" x-model="sourceEndpoint" required
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_path"
|
placeholder="https://webdav.example.com" />
|
||||||
id="source_path"
|
|
||||||
x-model="sourcePath"
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="/path/to/files"/>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Optional. Relative to the WebDAV URL.
|
Full URL to your WebDAV server including protocol (https://)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="source_user" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Username</label>
|
<label for="source_user" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-user text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="text" id="source_user" name="source_user" x-model="sourceUser" required
|
||||||
type="text"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
name="source_user"
|
placeholder="webdav_username" />
|
||||||
id="source_user"
|
|
||||||
x-model="sourceUser"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your WebDAV username
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
<div class="mb-6">
|
||||||
<label for="source_password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Password</label>
|
<label for="source_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="password" id="source_password" name="source_password" x-model="sourcePassword" required
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="WebDAV password" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your WebDAV account password
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="source_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Remote Path</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="source_path" name="source_path" x-model="sourcePath"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="/path/to/files" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Path to your files on the WebDAV server
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
|
||||||
|
<div class="flex">
|
||||||
|
<i class="fas fa-lightbulb mr-2 flex-shrink-0"></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium">Integration Note</h3>
|
||||||
|
<p class="mt-1">WebDAV is supported by many services including ownCloud, Nextcloud, Box, and many others. Make sure your WebDAV URL includes the full path to the WebDAV interface.</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="source_password"
|
|
||||||
id="source_password"
|
|
||||||
x-model="sourcePassword"
|
|
||||||
required
|
|
||||||
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
|
|
||||||
placeholder="Password"/>
|
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="source_password" :value="sourcePassword"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
+152
-82
@@ -6,98 +6,168 @@ import (
|
|||||||
|
|
||||||
templ Register(ctx context.Context, errorMessage string) {
|
templ Register(ctx context.Context, errorMessage string) {
|
||||||
@LayoutWithContext("Register", ctx) {
|
@LayoutWithContext("Register", ctx) {
|
||||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
<div class="p-4 md:p-6 2xl:p-10">
|
||||||
<div class="max-w-md w-full space-y-8">
|
<!-- Page Header -->
|
||||||
<div>
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
Create a new user account
|
<i class="fas fa-user-plus w-6 h-6 mr-2 text-blue-500"></i>
|
||||||
</h2>
|
Create Account
|
||||||
<p class="mt-2 text-center text-sm text-gray-600">
|
</h1>
|
||||||
Complete the form below to create a new user
|
</div>
|
||||||
</p>
|
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
||||||
</div>
|
Complete the form below to create a new user account
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Registration Card -->
|
||||||
|
<div class="max-w-md mx-auto">
|
||||||
if errorMessage != "" {
|
if errorMessage != "" {
|
||||||
<div class="mt-4">
|
<div class="p-4 mb-6 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-red-900/50 dark:text-red-400 border border-red-200 dark:border-red-800" role="alert">
|
||||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
|
<div class="flex items-center">
|
||||||
<span class="block sm:inline">{ errorMessage }</span>
|
<i class="fas fa-exclamation-circle flex-shrink-0 mr-2"></i>
|
||||||
|
<span>{ errorMessage }</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<form class="mt-8 space-y-6" hx-post="/register" hx-target="body"
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||||
x-data="{
|
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
email: '',
|
<div class="flex items-center space-x-3">
|
||||||
password: '',
|
<div class="h-10 w-10 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
|
||||||
confirmPassword: '',
|
<i class="fas fa-user-plus text-blue-600 dark:text-blue-300"></i>
|
||||||
loading: false,
|
</div>
|
||||||
validate() {
|
<div>
|
||||||
return this.email &&
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
this.password &&
|
Registration
|
||||||
this.confirmPassword &&
|
</h3>
|
||||||
this.password === this.confirmPassword;
|
<p class="text-sm text-gray-500 dark:text-gray-400">Create your account credentials</p>
|
||||||
}
|
</div>
|
||||||
}"
|
|
||||||
@htmx:before-request="loading = true"
|
|
||||||
@htmx:after-request="loading = false">
|
|
||||||
<input type="hidden" name="remember" value="true" />
|
|
||||||
<div class="rounded-md shadow-sm -space-y-px">
|
|
||||||
<div>
|
|
||||||
<label for="email" class="sr-only">Email address</label>
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
x-model="email"
|
|
||||||
class="form-input rounded-t-md"
|
|
||||||
placeholder="Email address"/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="password" class="sr-only">Password</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
x-model="password"
|
|
||||||
class="form-input"
|
|
||||||
placeholder="Password"/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="confirm-password" class="sr-only">Confirm Password</label>
|
|
||||||
<input
|
|
||||||
id="confirm-password"
|
|
||||||
name="confirm_password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
x-model="confirmPassword"
|
|
||||||
class="form-input rounded-b-md"
|
|
||||||
placeholder="Confirm Password"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<form
|
||||||
|
hx-post="/register"
|
||||||
|
hx-target="body"
|
||||||
|
class="space-y-4"
|
||||||
|
x-data="{
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
loading: false,
|
||||||
|
validate() {
|
||||||
|
return this.email &&
|
||||||
|
this.password &&
|
||||||
|
this.confirmPassword &&
|
||||||
|
this.password === this.confirmPassword;
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
@htmx:before-request="loading = true"
|
||||||
|
@htmx:after-request="loading = false">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
Email Address
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||||
|
<i class="fas fa-envelope text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
x-model="email"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="your.email@example.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||||
|
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
x-model="password"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="confirm-password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
Confirm Password
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||||
|
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="confirm-password"
|
||||||
|
name="confirm_password"
|
||||||
|
x-model="confirmPassword"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template x-if="confirmPassword && password !== confirmPassword">
|
||||||
|
<p class="mt-2 text-sm text-red-600 dark:text-red-500">
|
||||||
|
<i class="fas fa-exclamation-circle mr-1"></i>
|
||||||
|
Passwords do not match
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
||||||
|
x-bind:disabled="!validate() || loading">
|
||||||
|
<span x-show="!loading" class="flex items-center justify-center">
|
||||||
|
<i class="fas fa-user-plus mr-2"></i>
|
||||||
|
Create Account
|
||||||
|
</span>
|
||||||
|
<span x-show="loading" class="flex items-center justify-center">
|
||||||
|
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
Processing...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 bg-gray-50 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 rounded-b-lg">
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Already have an account?
|
||||||
|
<a href="/login" class="font-medium text-blue-600 dark:text-blue-500 hover:underline">
|
||||||
|
Sign in
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Help Notice -->
|
||||||
|
<div class="mt-6 max-w-md mx-auto">
|
||||||
|
<div class="p-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/50 dark:text-blue-400 border border-blue-200 dark:border-blue-800 flex items-start">
|
||||||
|
<i class="fas fa-info-circle flex-shrink-0 mr-3 mt-0.5"></i>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<h4 class="text-sm font-medium mb-1">Account Information</h4>
|
||||||
type="submit"
|
<p>Creating an account will allow you to manage file transfers and configurations. Your email address will only be used for authentication and important notifications related to your transfers.</p>
|
||||||
class="btn-primary w-full flex justify-center"
|
|
||||||
x-bind:disabled="!validate() || loading">
|
|
||||||
<span x-show="!loading">Create User</span>
|
|
||||||
<span x-show="loading" class="flex items-center">
|
|
||||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
Processing...
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
<div class="text-center">
|
|
||||||
<p class="text-sm text-gray-600">
|
|
||||||
Already have an account?
|
|
||||||
<a href="/login" class="font-medium text-blue-600 hover:text-blue-500">Sign in</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user