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.contains("jetty")
|
||||
|| fileName.startsWith("jetty-")
|
||||
|| fileName.equals("proc")
|
||||
|| fileName.equals("sys")
|
||||
|| fileName.equals("dev")
|
||||
|| fileName.equals("hsperfdata_stirlingpdfuser")
|
||||
|| "proc".equals(fileName)
|
||||
|| "sys".equals(fileName)
|
||||
|| "dev".equals(fileName)
|
||||
|| "hsperfdata_stirlingpdfuser".equals(fileName)
|
||||
|| fileName.startsWith("hsperfdata_")
|
||||
|| fileName.equals(".pdfbox.cache");
|
||||
|| ".pdfbox.cache".equals(fileName);
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
|
@ -9,7 +9,6 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.ConcurrentSkipListSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
@ -24,11 +23,10 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Component
|
||||
public class TempFileRegistry {
|
||||
|
||||
private final ConcurrentMap<Path, Instant> registeredFiles = new ConcurrentHashMap<>();
|
||||
private final Set<Path> thirdPartyTempFiles =
|
||||
Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
private final Set<Path> tempDirectories =
|
||||
Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
private final ConcurrentMap<Path, Instant> registeredFiles = new ConcurrentHashMap<>();
|
||||
private final Set<Path> thirdPartyTempFiles =
|
||||
Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
private final Set<Path> tempDirectories = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
|
||||
/**
|
||||
* 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 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 AppsIcon from "@mui/icons-material/Apps";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import { useRainbowThemeContext } from "./RainbowThemeProvider";
|
||||
import rainbowStyles from '../../styles/rainbow.module.css';
|
||||
import AppConfigModal from './AppConfigModal';
|
||||
|
||||
interface QuickAccessBarProps {
|
||||
onToolsClick: () => void;
|
||||
@ -26,6 +25,7 @@ const QuickAccessBar = ({
|
||||
readerMode,
|
||||
}: QuickAccessBarProps) => {
|
||||
const { isRainbowMode } = useRainbowThemeContext();
|
||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -62,7 +62,24 @@ const QuickAccessBar = ({
|
||||
|
||||
{/* Spacer */}
|
||||
<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>
|
||||
|
||||
<AppConfigModal
|
||||
opened={configModalOpen}
|
||||
onClose={() => setConfigModalOpen(false)}
|
||||
/>
|
||||
</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 ToolRenderer from "../components/tools/ToolRenderer";
|
||||
import QuickAccessBar from "../components/shared/QuickAccessBar";
|
||||
import { useMultipleEndpointsEnabled } from "../hooks/useEndpointConfig";
|
||||
|
||||
type ToolRegistryEntry = {
|
||||
icon: React.ReactNode;
|
||||
@ -43,6 +44,13 @@ const baseToolRegistry = {
|
||||
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() {
|
||||
const { t } = useTranslation();
|
||||
const [searchParams] = useSearchParams();
|
||||
@ -69,6 +77,10 @@ export default function HomePage() {
|
||||
// URL parameter management
|
||||
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
|
||||
useEffect(() => {
|
||||
// Save active files to localStorage (just metadata)
|
||||
@ -110,12 +122,41 @@ export default function HomePage() {
|
||||
restoreActiveFiles();
|
||||
}, []);
|
||||
|
||||
const toolRegistry: ToolRegistry = {
|
||||
split: { ...baseToolRegistry.split, name: t("home.split.title", "Split PDF") },
|
||||
compress: { ...baseToolRegistry.compress, name: t("home.compressPdfs.title", "Compress PDF") },
|
||||
merge: { ...baseToolRegistry.merge, name: t("home.merge.title", "Merge PDFs") },
|
||||
// Helper function to check if a tool is available
|
||||
const isToolAvailable = (toolKey: string): boolean => {
|
||||
if (endpointsLoading) return true; // Show tools while loading
|
||||
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
|
||||
const handleToolSelect = useCallback(
|
||||
(id: string) => {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
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 { fileStorage } from "../services/fileStorage";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
|
||||
export interface CompressProps {
|
||||
files?: FileWithUrl[];
|
||||
@ -36,6 +36,7 @@ const CompressPdfPanel: React.FC<CompressProps> = ({
|
||||
|
||||
const [selected, setSelected] = useState<boolean[]>(files.map(() => false));
|
||||
const [localLoading, setLocalLoading] = useState<boolean>(false);
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("compress-pdf");
|
||||
|
||||
const {
|
||||
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 (
|
||||
<Stack>
|
||||
|
@ -4,6 +4,7 @@ import { useSearchParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FileWithUrl } from "../types/file";
|
||||
import { fileStorage } from "../services/fileStorage";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
|
||||
export interface MergePdfPanelProps {
|
||||
files: FileWithUrl[];
|
||||
@ -22,11 +23,11 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({
|
||||
updateParams,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [selectedFiles, setSelectedFiles] = useState<boolean[]>([]);
|
||||
const [downloadUrl, setLocalDownloadUrl] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("merge-pdfs");
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedFiles(files.map(() => true));
|
||||
@ -92,6 +93,25 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({
|
||||
|
||||
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 (
|
||||
<Stack>
|
||||
<Text fw={500} size="lg">{t("merge.header")}</Text>
|
||||
|
@ -7,13 +7,16 @@ import {
|
||||
Checkbox,
|
||||
Notification,
|
||||
Stack,
|
||||
Paper,
|
||||
Loader,
|
||||
Alert,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import { FileWithUrl } from "../types/file";
|
||||
import { fileStorage } from "../services/fileStorage";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
|
||||
export interface SplitPdfPanelProps {
|
||||
file: { file: FileWithUrl; url: string } | null;
|
||||
@ -48,6 +51,23 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
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 {
|
||||
mode,
|
||||
pages,
|
||||
@ -62,6 +82,7 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
||||
} = params;
|
||||
|
||||
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(getEndpointName(mode));
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
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 (
|
||||
<form onSubmit={handleSubmit} className="app-surface p-app-md rounded-app-md">
|
||||
<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