mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-07-27 15:45:21 +00:00
Merge pull request #3833 from Stirling-Tools/feature/react-backend-config-access
Feature/react backend config access
This commit is contained in:
commit
50d745225c
@ -72,12 +72,12 @@ public class TempFileCleanupService {
|
|||||||
fileName ->
|
fileName ->
|
||||||
fileName.contains("jetty")
|
fileName.contains("jetty")
|
||||||
|| fileName.startsWith("jetty-")
|
|| fileName.startsWith("jetty-")
|
||||||
|| fileName.equals("proc")
|
|| "proc".equals(fileName)
|
||||||
|| fileName.equals("sys")
|
|| "sys".equals(fileName)
|
||||||
|| fileName.equals("dev")
|
|| "dev".equals(fileName)
|
||||||
|| fileName.equals("hsperfdata_stirlingpdfuser")
|
|| "hsperfdata_stirlingpdfuser".equals(fileName)
|
||||||
|| fileName.startsWith("hsperfdata_")
|
|| fileName.startsWith("hsperfdata_")
|
||||||
|| fileName.equals(".pdfbox.cache");
|
|| ".pdfbox.cache".equals(fileName);
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
|
@ -9,7 +9,6 @@ import java.util.Map;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ConcurrentMap;
|
import java.util.concurrent.ConcurrentMap;
|
||||||
import java.util.concurrent.ConcurrentSkipListSet;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@ -27,8 +26,7 @@ public class TempFileRegistry {
|
|||||||
private final ConcurrentMap<Path, Instant> registeredFiles = new ConcurrentHashMap<>();
|
private final ConcurrentMap<Path, Instant> registeredFiles = new ConcurrentHashMap<>();
|
||||||
private final Set<Path> thirdPartyTempFiles =
|
private final Set<Path> thirdPartyTempFiles =
|
||||||
Collections.newSetFromMap(new ConcurrentHashMap<>());
|
Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||||
private final Set<Path> tempDirectories =
|
private final Set<Path> tempDirectories = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||||
Collections.newSetFromMap(new ConcurrentHashMap<>());
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a temporary file with the registry.
|
* Register a temporary file with the registry.
|
||||||
|
140
frontend/src/components/shared/AppConfigModal.tsx
Normal file
140
frontend/src/components/shared/AppConfigModal.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Modal, Button, Stack, Text, Code, ScrollArea, Group, Badge, Alert, Loader } from '@mantine/core';
|
||||||
|
import { useAppConfig } from '../../hooks/useAppConfig';
|
||||||
|
|
||||||
|
interface AppConfigModalProps {
|
||||||
|
opened: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
|
||||||
|
const { config, loading, error, refetch } = useAppConfig();
|
||||||
|
|
||||||
|
const renderConfigSection = (title: string, data: any) => {
|
||||||
|
if (!data || typeof data !== 'object') return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="xs" mb="md">
|
||||||
|
<Text fw={600} size="md" c="blue">{title}</Text>
|
||||||
|
<Stack gap="xs" pl="md">
|
||||||
|
{Object.entries(data).map(([key, value]) => (
|
||||||
|
<Group key={key} wrap="nowrap" align="flex-start">
|
||||||
|
<Text size="sm" w={150} style={{ flexShrink: 0 }} c="dimmed">
|
||||||
|
{key}:
|
||||||
|
</Text>
|
||||||
|
{typeof value === 'boolean' ? (
|
||||||
|
<Badge color={value ? 'green' : 'red'} size="sm">
|
||||||
|
{value ? 'true' : 'false'}
|
||||||
|
</Badge>
|
||||||
|
) : typeof value === 'object' ? (
|
||||||
|
<Code block>{JSON.stringify(value, null, 2)}</Code>
|
||||||
|
) : (
|
||||||
|
String(value) || 'null'
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const basicConfig = config ? {
|
||||||
|
appName: config.appName,
|
||||||
|
appNameNavbar: config.appNameNavbar,
|
||||||
|
baseUrl: config.baseUrl,
|
||||||
|
contextPath: config.contextPath,
|
||||||
|
serverPort: config.serverPort,
|
||||||
|
} : null;
|
||||||
|
|
||||||
|
const securityConfig = config ? {
|
||||||
|
enableLogin: config.enableLogin,
|
||||||
|
} : null;
|
||||||
|
|
||||||
|
const systemConfig = config ? {
|
||||||
|
enableAlphaFunctionality: config.enableAlphaFunctionality,
|
||||||
|
enableAnalytics: config.enableAnalytics,
|
||||||
|
} : null;
|
||||||
|
|
||||||
|
const premiumConfig = config ? {
|
||||||
|
premiumEnabled: config.premiumEnabled,
|
||||||
|
premiumKey: config.premiumKey ? '***hidden***' : null,
|
||||||
|
runningProOrHigher: config.runningProOrHigher,
|
||||||
|
runningEE: config.runningEE,
|
||||||
|
license: config.license,
|
||||||
|
} : null;
|
||||||
|
|
||||||
|
const integrationConfig = config ? {
|
||||||
|
GoogleDriveEnabled: config.GoogleDriveEnabled,
|
||||||
|
SSOAutoLogin: config.SSOAutoLogin,
|
||||||
|
} : null;
|
||||||
|
|
||||||
|
const legalConfig = config ? {
|
||||||
|
termsAndConditions: config.termsAndConditions,
|
||||||
|
privacyPolicy: config.privacyPolicy,
|
||||||
|
cookiePolicy: config.cookiePolicy,
|
||||||
|
impressum: config.impressum,
|
||||||
|
accessibilityStatement: config.accessibilityStatement,
|
||||||
|
} : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={onClose}
|
||||||
|
title="App Configuration (Testing)"
|
||||||
|
size="lg"
|
||||||
|
style={{ zIndex: 1000 }}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
This modal shows the current application configuration for testing purposes only.
|
||||||
|
</Text>
|
||||||
|
<Button size="xs" variant="light" onClick={refetch}>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<Stack align="center" py="md">
|
||||||
|
<Loader size="sm" />
|
||||||
|
<Text size="sm" c="dimmed">Loading configuration...</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert color="red" title="Error">
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{config && (
|
||||||
|
<ScrollArea h={400}>
|
||||||
|
<Stack gap="lg">
|
||||||
|
{renderConfigSection('Basic Configuration', basicConfig)}
|
||||||
|
{renderConfigSection('Security Configuration', securityConfig)}
|
||||||
|
{renderConfigSection('System Configuration', systemConfig)}
|
||||||
|
{renderConfigSection('Premium/Enterprise Configuration', premiumConfig)}
|
||||||
|
{renderConfigSection('Integration Configuration', integrationConfig)}
|
||||||
|
{renderConfigSection('Legal Configuration', legalConfig)}
|
||||||
|
|
||||||
|
{config.error && (
|
||||||
|
<Alert color="yellow" title="Configuration Warning">
|
||||||
|
{config.error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text fw={600} size="md" c="blue">Raw Configuration</Text>
|
||||||
|
<Code block style={{ fontSize: '11px' }}>
|
||||||
|
{JSON.stringify(config, null, 2)}
|
||||||
|
</Code>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</ScrollArea>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppConfigModal;
|
@ -1,12 +1,11 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { ActionIcon, Stack, Tooltip } from "@mantine/core";
|
import { ActionIcon, Stack, Tooltip } from "@mantine/core";
|
||||||
import AddToPhotosIcon from "@mui/icons-material/AddToPhotos";
|
|
||||||
import ContentCutIcon from "@mui/icons-material/ContentCut";
|
|
||||||
import ZoomInMapIcon from "@mui/icons-material/ZoomInMap";
|
|
||||||
import MenuBookIcon from "@mui/icons-material/MenuBook";
|
import MenuBookIcon from "@mui/icons-material/MenuBook";
|
||||||
import AppsIcon from "@mui/icons-material/Apps";
|
import AppsIcon from "@mui/icons-material/Apps";
|
||||||
|
import SettingsIcon from "@mui/icons-material/Settings";
|
||||||
import { useRainbowThemeContext } from "./RainbowThemeProvider";
|
import { useRainbowThemeContext } from "./RainbowThemeProvider";
|
||||||
import rainbowStyles from '../../styles/rainbow.module.css';
|
import rainbowStyles from '../../styles/rainbow.module.css';
|
||||||
|
import AppConfigModal from './AppConfigModal';
|
||||||
|
|
||||||
interface QuickAccessBarProps {
|
interface QuickAccessBarProps {
|
||||||
onToolsClick: () => void;
|
onToolsClick: () => void;
|
||||||
@ -26,6 +25,7 @@ const QuickAccessBar = ({
|
|||||||
readerMode,
|
readerMode,
|
||||||
}: QuickAccessBarProps) => {
|
}: QuickAccessBarProps) => {
|
||||||
const { isRainbowMode } = useRainbowThemeContext();
|
const { isRainbowMode } = useRainbowThemeContext();
|
||||||
|
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -62,7 +62,24 @@ const QuickAccessBar = ({
|
|||||||
|
|
||||||
{/* Spacer */}
|
{/* Spacer */}
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
|
|
||||||
|
{/* Config Modal Button (for testing) */}
|
||||||
|
<div className="flex flex-col items-center gap-1">
|
||||||
|
<ActionIcon
|
||||||
|
size="lg"
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => setConfigModalOpen(true)}
|
||||||
|
>
|
||||||
|
<SettingsIcon sx={{ fontSize: 20 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
<span className="text-xs text-center leading-tight" style={{ color: 'var(--text-secondary)' }}>Config</span>
|
||||||
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
<AppConfigModal
|
||||||
|
opened={configModalOpen}
|
||||||
|
onClose={() => setConfigModalOpen(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
77
frontend/src/hooks/useAppConfig.ts
Normal file
77
frontend/src/hooks/useAppConfig.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export interface AppConfig {
|
||||||
|
baseUrl?: string;
|
||||||
|
contextPath?: string;
|
||||||
|
serverPort?: number;
|
||||||
|
appName?: string;
|
||||||
|
appNameNavbar?: string;
|
||||||
|
homeDescription?: string;
|
||||||
|
languages?: string[];
|
||||||
|
enableLogin?: boolean;
|
||||||
|
enableAlphaFunctionality?: boolean;
|
||||||
|
enableAnalytics?: boolean;
|
||||||
|
premiumEnabled?: boolean;
|
||||||
|
premiumKey?: string;
|
||||||
|
termsAndConditions?: string;
|
||||||
|
privacyPolicy?: string;
|
||||||
|
cookiePolicy?: string;
|
||||||
|
impressum?: string;
|
||||||
|
accessibilityStatement?: string;
|
||||||
|
runningProOrHigher?: boolean;
|
||||||
|
runningEE?: boolean;
|
||||||
|
license?: string;
|
||||||
|
GoogleDriveEnabled?: boolean;
|
||||||
|
SSOAutoLogin?: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseAppConfigReturn {
|
||||||
|
config: AppConfig | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
refetch: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook to fetch and manage application configuration
|
||||||
|
*/
|
||||||
|
export function useAppConfig(): UseAppConfigReturn {
|
||||||
|
const [config, setConfig] = useState<AppConfig | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchConfig = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const response = await fetch('/api/v1/config/app-config');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch config: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: AppConfig = await response.json();
|
||||||
|
setConfig(data);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
||||||
|
setError(errorMessage);
|
||||||
|
console.error('Failed to fetch app config:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchConfig();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
config,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch: fetchConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
117
frontend/src/hooks/useEndpointConfig.ts
Normal file
117
frontend/src/hooks/useEndpointConfig.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to check if a specific endpoint is enabled
|
||||||
|
*/
|
||||||
|
export function useEndpointEnabled(endpoint: string): {
|
||||||
|
enabled: boolean | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
refetch: () => Promise<void>;
|
||||||
|
} {
|
||||||
|
const [enabled, setEnabled] = useState<boolean | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchEndpointStatus = async () => {
|
||||||
|
if (!endpoint) {
|
||||||
|
setEnabled(null);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const response = await fetch(`/api/v1/config/endpoint-enabled?endpoint=${encodeURIComponent(endpoint)}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to check endpoint: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEnabled: boolean = await response.json();
|
||||||
|
setEnabled(isEnabled);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
||||||
|
setError(errorMessage);
|
||||||
|
console.error(`Failed to check endpoint ${endpoint}:`, err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchEndpointStatus();
|
||||||
|
}, [endpoint]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch: fetchEndpointStatus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to check multiple endpoints at once using batch API
|
||||||
|
* Returns a map of endpoint -> enabled status
|
||||||
|
*/
|
||||||
|
export function useMultipleEndpointsEnabled(endpoints: string[]): {
|
||||||
|
endpointStatus: Record<string, boolean>;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
refetch: () => Promise<void>;
|
||||||
|
} {
|
||||||
|
const [endpointStatus, setEndpointStatus] = useState<Record<string, boolean>>({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchAllEndpointStatuses = async () => {
|
||||||
|
if (!endpoints || endpoints.length === 0) {
|
||||||
|
setEndpointStatus({});
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Use batch API for efficiency
|
||||||
|
const endpointsParam = endpoints.join(',');
|
||||||
|
const response = await fetch(`/api/v1/config/endpoints-enabled?endpoints=${encodeURIComponent(endpointsParam)}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to check endpoints: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusMap: Record<string, boolean> = await response.json();
|
||||||
|
setEndpointStatus(statusMap);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
||||||
|
setError(errorMessage);
|
||||||
|
console.error('Failed to check multiple endpoints:', err);
|
||||||
|
|
||||||
|
// Fallback: assume all endpoints are disabled on error
|
||||||
|
const fallbackStatus = endpoints.reduce((acc, endpoint) => {
|
||||||
|
acc[endpoint] = false;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, boolean>);
|
||||||
|
setEndpointStatus(fallbackStatus);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAllEndpointStatuses();
|
||||||
|
}, [endpoints.join(',')]); // Re-run when endpoints array changes
|
||||||
|
|
||||||
|
return {
|
||||||
|
endpointStatus,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch: fetchAllEndpointStatuses,
|
||||||
|
};
|
||||||
|
}
|
@ -24,6 +24,7 @@ import CompressPdfPanel from "../tools/Compress";
|
|||||||
import MergePdfPanel from "../tools/Merge";
|
import MergePdfPanel from "../tools/Merge";
|
||||||
import ToolRenderer from "../components/tools/ToolRenderer";
|
import ToolRenderer from "../components/tools/ToolRenderer";
|
||||||
import QuickAccessBar from "../components/shared/QuickAccessBar";
|
import QuickAccessBar from "../components/shared/QuickAccessBar";
|
||||||
|
import { useMultipleEndpointsEnabled } from "../hooks/useEndpointConfig";
|
||||||
|
|
||||||
type ToolRegistryEntry = {
|
type ToolRegistryEntry = {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
@ -43,6 +44,13 @@ const baseToolRegistry = {
|
|||||||
merge: { icon: <AddToPhotosIcon />, component: MergePdfPanel, view: "fileManager" },
|
merge: { icon: <AddToPhotosIcon />, component: MergePdfPanel, view: "fileManager" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Tool endpoint mappings
|
||||||
|
const toolEndpoints: Record<string, string[]> = {
|
||||||
|
split: ["split-pages", "split-pdf-by-sections", "split-by-size-or-count", "split-pdf-by-chapters"],
|
||||||
|
compress: ["compress-pdf"],
|
||||||
|
merge: ["merge-pdfs"],
|
||||||
|
};
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
@ -69,6 +77,10 @@ export default function HomePage() {
|
|||||||
// URL parameter management
|
// URL parameter management
|
||||||
const { toolParams, updateParams } = useToolParams(selectedToolKey, currentView);
|
const { toolParams, updateParams } = useToolParams(selectedToolKey, currentView);
|
||||||
|
|
||||||
|
// Get all unique endpoints for batch checking
|
||||||
|
const allEndpoints = Array.from(new Set(Object.values(toolEndpoints).flat()));
|
||||||
|
const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints);
|
||||||
|
|
||||||
// Persist active files across reloads
|
// Persist active files across reloads
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Save active files to localStorage (just metadata)
|
// Save active files to localStorage (just metadata)
|
||||||
@ -110,12 +122,41 @@ export default function HomePage() {
|
|||||||
restoreActiveFiles();
|
restoreActiveFiles();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toolRegistry: ToolRegistry = {
|
// Helper function to check if a tool is available
|
||||||
split: { ...baseToolRegistry.split, name: t("home.split.title", "Split PDF") },
|
const isToolAvailable = (toolKey: string): boolean => {
|
||||||
compress: { ...baseToolRegistry.compress, name: t("home.compressPdfs.title", "Compress PDF") },
|
if (endpointsLoading) return true; // Show tools while loading
|
||||||
merge: { ...baseToolRegistry.merge, name: t("home.merge.title", "Merge PDFs") },
|
const endpoints = toolEndpoints[toolKey] || [];
|
||||||
|
// Tool is available if at least one of its endpoints is enabled
|
||||||
|
return endpoints.some(endpoint => endpointStatus[endpoint] === true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Filter tool registry to only show available tools
|
||||||
|
const availableToolRegistry: ToolRegistry = {};
|
||||||
|
Object.keys(baseToolRegistry).forEach(toolKey => {
|
||||||
|
if (isToolAvailable(toolKey)) {
|
||||||
|
availableToolRegistry[toolKey] = {
|
||||||
|
...baseToolRegistry[toolKey as keyof typeof baseToolRegistry],
|
||||||
|
name: t(`home.${toolKey}.title`, toolKey.charAt(0).toUpperCase() + toolKey.slice(1))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const toolRegistry = availableToolRegistry;
|
||||||
|
|
||||||
|
// Handle case where selected tool becomes unavailable
|
||||||
|
useEffect(() => {
|
||||||
|
if (!endpointsLoading && selectedToolKey && !toolRegistry[selectedToolKey]) {
|
||||||
|
// If current tool is not available, select the first available tool
|
||||||
|
const firstAvailableTool = Object.keys(toolRegistry)[0];
|
||||||
|
if (firstAvailableTool) {
|
||||||
|
setSelectedToolKey(firstAvailableTool);
|
||||||
|
if (toolRegistry[firstAvailableTool]?.view) {
|
||||||
|
setCurrentView(toolRegistry[firstAvailableTool].view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [endpointsLoading, selectedToolKey, toolRegistry]);
|
||||||
|
|
||||||
// Handle tool selection
|
// Handle tool selection
|
||||||
const handleToolSelect = useCallback(
|
const handleToolSelect = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Stack, Slider, Group, Text, Button, Checkbox, TextInput, Paper } from "@mantine/core";
|
import { Stack, Slider, Group, Text, Button, Checkbox, TextInput, Loader, Alert } from "@mantine/core";
|
||||||
import { FileWithUrl } from "../types/file";
|
import { FileWithUrl } from "../types/file";
|
||||||
import { fileStorage } from "../services/fileStorage";
|
import { fileStorage } from "../services/fileStorage";
|
||||||
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||||
|
|
||||||
export interface CompressProps {
|
export interface CompressProps {
|
||||||
files?: FileWithUrl[];
|
files?: FileWithUrl[];
|
||||||
@ -36,6 +36,7 @@ const CompressPdfPanel: React.FC<CompressProps> = ({
|
|||||||
|
|
||||||
const [selected, setSelected] = useState<boolean[]>(files.map(() => false));
|
const [selected, setSelected] = useState<boolean[]>(files.map(() => false));
|
||||||
const [localLoading, setLocalLoading] = useState<boolean>(false);
|
const [localLoading, setLocalLoading] = useState<boolean>(false);
|
||||||
|
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("compress-pdf");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
compressionLevel,
|
compressionLevel,
|
||||||
@ -99,6 +100,24 @@ const CompressPdfPanel: React.FC<CompressProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (endpointLoading) {
|
||||||
|
return (
|
||||||
|
<Stack align="center" justify="center" h={200}>
|
||||||
|
<Loader size="md" />
|
||||||
|
<Text size="sm" c="dimmed">{t("loading", "Loading...")}</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endpointEnabled === false) {
|
||||||
|
return (
|
||||||
|
<Stack align="center" justify="center" h={200}>
|
||||||
|
<Alert color="red" title={t("error._value", "Error")} variant="light">
|
||||||
|
{t("endpointDisabled", "This feature is currently disabled.")}
|
||||||
|
</Alert>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
|
@ -4,6 +4,7 @@ import { useSearchParams } from "react-router-dom";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { FileWithUrl } from "../types/file";
|
import { FileWithUrl } from "../types/file";
|
||||||
import { fileStorage } from "../services/fileStorage";
|
import { fileStorage } from "../services/fileStorage";
|
||||||
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||||
|
|
||||||
export interface MergePdfPanelProps {
|
export interface MergePdfPanelProps {
|
||||||
files: FileWithUrl[];
|
files: FileWithUrl[];
|
||||||
@ -22,11 +23,11 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({
|
|||||||
updateParams,
|
updateParams,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const [selectedFiles, setSelectedFiles] = useState<boolean[]>([]);
|
const [selectedFiles, setSelectedFiles] = useState<boolean[]>([]);
|
||||||
const [downloadUrl, setLocalDownloadUrl] = useState<string | null>(null);
|
const [downloadUrl, setLocalDownloadUrl] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("merge-pdfs");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedFiles(files.map(() => true));
|
setSelectedFiles(files.map(() => true));
|
||||||
@ -92,6 +93,25 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({
|
|||||||
|
|
||||||
const { order, removeDuplicates } = params;
|
const { order, removeDuplicates } = params;
|
||||||
|
|
||||||
|
if (endpointLoading) {
|
||||||
|
return (
|
||||||
|
<Stack align="center" justify="center" h={200}>
|
||||||
|
<Loader size="md" />
|
||||||
|
<Text size="sm" c="dimmed">{t("loading", "Loading...")}</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endpointEnabled === false) {
|
||||||
|
return (
|
||||||
|
<Stack align="center" justify="center" h={200}>
|
||||||
|
<Alert color="red" title={t("error._value", "Error")} variant="light">
|
||||||
|
{t("endpointDisabled", "This feature is currently disabled.")}
|
||||||
|
</Alert>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text fw={500} size="lg">{t("merge.header")}</Text>
|
<Text fw={500} size="lg">{t("merge.header")}</Text>
|
||||||
|
@ -7,13 +7,16 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
Notification,
|
Notification,
|
||||||
Stack,
|
Stack,
|
||||||
Paper,
|
Loader,
|
||||||
|
Alert,
|
||||||
|
Text,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import DownloadIcon from "@mui/icons-material/Download";
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
import { FileWithUrl } from "../types/file";
|
import { FileWithUrl } from "../types/file";
|
||||||
import { fileStorage } from "../services/fileStorage";
|
import { fileStorage } from "../services/fileStorage";
|
||||||
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||||
|
|
||||||
export interface SplitPdfPanelProps {
|
export interface SplitPdfPanelProps {
|
||||||
file: { file: FileWithUrl; url: string } | null;
|
file: { file: FileWithUrl; url: string } | null;
|
||||||
@ -48,6 +51,23 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Map mode to endpoint name for checking
|
||||||
|
const getEndpointName = (mode: string) => {
|
||||||
|
switch (mode) {
|
||||||
|
case "byPages":
|
||||||
|
return "split-pages";
|
||||||
|
case "bySections":
|
||||||
|
return "split-pdf-by-sections";
|
||||||
|
case "bySizeOrCount":
|
||||||
|
return "split-by-size-or-count";
|
||||||
|
case "byChapters":
|
||||||
|
return "split-pdf-by-chapters";
|
||||||
|
default:
|
||||||
|
return "split-pages";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mode,
|
mode,
|
||||||
pages,
|
pages,
|
||||||
@ -62,6 +82,7 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
|||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
|
|
||||||
|
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(getEndpointName(mode));
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!file) {
|
if (!file) {
|
||||||
@ -142,6 +163,25 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (endpointLoading) {
|
||||||
|
return (
|
||||||
|
<Stack align="center" justify="center" h={200}>
|
||||||
|
<Loader size="md" />
|
||||||
|
<Text size="sm" c="dimmed">{t("loading", "Loading...")}</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endpointEnabled === false) {
|
||||||
|
return (
|
||||||
|
<Stack align="center" justify="center" h={200}>
|
||||||
|
<Alert color="red" title={t("error._value", "Error")} variant="light">
|
||||||
|
{t("endpointDisabled", "This feature is currently disabled.")}
|
||||||
|
</Alert>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="app-surface p-app-md rounded-app-md">
|
<form onSubmit={handleSubmit} className="app-surface p-app-md rounded-app-md">
|
||||||
<Stack gap="sm" mb={16}>
|
<Stack gap="sm" mb={16}>
|
||||||
|
@ -0,0 +1,129 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||||
|
import stirling.software.common.configuration.AppConfig;
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@Tag(name = "Config", description = "Configuration APIs")
|
||||||
|
@RequestMapping("/api/v1/config")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Hidden
|
||||||
|
public class ConfigController {
|
||||||
|
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
private final ApplicationContext applicationContext;
|
||||||
|
private final EndpointConfiguration endpointConfiguration;
|
||||||
|
|
||||||
|
@GetMapping("/app-config")
|
||||||
|
public ResponseEntity<Map<String, Object>> getAppConfig() {
|
||||||
|
Map<String, Object> configData = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get AppConfig bean
|
||||||
|
AppConfig appConfig = applicationContext.getBean(AppConfig.class);
|
||||||
|
|
||||||
|
// Extract key configuration values from AppConfig
|
||||||
|
configData.put("baseUrl", appConfig.getBaseUrl());
|
||||||
|
configData.put("contextPath", appConfig.getContextPath());
|
||||||
|
configData.put("serverPort", appConfig.getServerPort());
|
||||||
|
|
||||||
|
// Extract values from ApplicationProperties
|
||||||
|
configData.put("appName", applicationProperties.getUi().getAppName());
|
||||||
|
configData.put("appNameNavbar", applicationProperties.getUi().getAppNameNavbar());
|
||||||
|
configData.put("homeDescription", applicationProperties.getUi().getHomeDescription());
|
||||||
|
configData.put("languages", applicationProperties.getUi().getLanguages());
|
||||||
|
|
||||||
|
// Security settings
|
||||||
|
configData.put("enableLogin", applicationProperties.getSecurity().getEnableLogin());
|
||||||
|
|
||||||
|
// System settings
|
||||||
|
configData.put(
|
||||||
|
"enableAlphaFunctionality",
|
||||||
|
applicationProperties.getSystem().getEnableAlphaFunctionality());
|
||||||
|
configData.put(
|
||||||
|
"enableAnalytics", applicationProperties.getSystem().getEnableAnalytics());
|
||||||
|
|
||||||
|
// Premium/Enterprise settings
|
||||||
|
configData.put("premiumEnabled", applicationProperties.getPremium().isEnabled());
|
||||||
|
|
||||||
|
// Legal settings
|
||||||
|
configData.put(
|
||||||
|
"termsAndConditions", applicationProperties.getLegal().getTermsAndConditions());
|
||||||
|
configData.put("privacyPolicy", applicationProperties.getLegal().getPrivacyPolicy());
|
||||||
|
configData.put("cookiePolicy", applicationProperties.getLegal().getCookiePolicy());
|
||||||
|
configData.put("impressum", applicationProperties.getLegal().getImpressum());
|
||||||
|
configData.put(
|
||||||
|
"accessibilityStatement",
|
||||||
|
applicationProperties.getLegal().getAccessibilityStatement());
|
||||||
|
|
||||||
|
// Try to get EEAppConfig values if available
|
||||||
|
try {
|
||||||
|
if (applicationContext.containsBean("runningProOrHigher")) {
|
||||||
|
configData.put(
|
||||||
|
"runningProOrHigher",
|
||||||
|
applicationContext.getBean("runningProOrHigher", Boolean.class));
|
||||||
|
}
|
||||||
|
if (applicationContext.containsBean("runningEE")) {
|
||||||
|
configData.put(
|
||||||
|
"runningEE", applicationContext.getBean("runningEE", Boolean.class));
|
||||||
|
}
|
||||||
|
if (applicationContext.containsBean("license")) {
|
||||||
|
configData.put("license", applicationContext.getBean("license", String.class));
|
||||||
|
}
|
||||||
|
if (applicationContext.containsBean("GoogleDriveEnabled")) {
|
||||||
|
configData.put(
|
||||||
|
"GoogleDriveEnabled",
|
||||||
|
applicationContext.getBean("GoogleDriveEnabled", Boolean.class));
|
||||||
|
}
|
||||||
|
if (applicationContext.containsBean("SSOAutoLogin")) {
|
||||||
|
configData.put(
|
||||||
|
"SSOAutoLogin",
|
||||||
|
applicationContext.getBean("SSOAutoLogin", Boolean.class));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// EE features not available, continue without them
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(configData);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Return basic config if there are any issues
|
||||||
|
configData.put("error", "Unable to retrieve full configuration");
|
||||||
|
return ResponseEntity.ok(configData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/endpoint-enabled")
|
||||||
|
public ResponseEntity<Boolean> isEndpointEnabled(@RequestParam String endpoint) {
|
||||||
|
boolean enabled = endpointConfiguration.isEndpointEnabled(endpoint);
|
||||||
|
return ResponseEntity.ok(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/endpoints-enabled")
|
||||||
|
public ResponseEntity<Map<String, Boolean>> areEndpointsEnabled(
|
||||||
|
@RequestParam String endpoints) {
|
||||||
|
Map<String, Boolean> result = new HashMap<>();
|
||||||
|
String[] endpointArray = endpoints.split(",");
|
||||||
|
for (String endpoint : endpointArray) {
|
||||||
|
String trimmedEndpoint = endpoint.trim();
|
||||||
|
result.put(trimmedEndpoint, endpointConfiguration.isEndpointEnabled(trimmedEndpoint));
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user