From 30461cd91ec312295dcb2f2fbf96529c764b19a5 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Tue, 29 Jul 2025 11:50:15 +0100 Subject: [PATCH] formatting and UI --- .../SPDF/controller/api/UIDataController.java | 128 ++++++++------ .../api/ProprietaryUIDataController.java | 164 ++++++++++-------- docker/frontend/nginx.conf | 6 +- .../public/locales/en-GB/translation.json | 10 ++ .../public/locales/en-US/translation.json | 10 ++ frontend/src/hooks/useToolManagement.tsx | 10 ++ frontend/src/tools/SwaggerUI.tsx | 18 ++ 7 files changed, 221 insertions(+), 125 deletions(-) create mode 100644 frontend/src/tools/SwaggerUI.tsx diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java index 943fb1a52..f5b1a70f1 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java @@ -9,7 +9,6 @@ import java.nio.file.Paths; import java.util.*; import java.util.stream.Stream; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; @@ -35,7 +34,6 @@ import stirling.software.common.configuration.InstallationPathConfig; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.UserServiceInterface; -import stirling.software.common.util.CheckProgramInstall; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -57,10 +55,10 @@ public class UIDataController { public ResponseEntity getHomeData() { String showSurvey = System.getenv("SHOW_SURVEY"); boolean showSurveyValue = showSurvey == null || "true".equalsIgnoreCase(showSurvey); - + HomeData data = new HomeData(); data.setShowSurveyFromDocker(showSurveyValue); - + return ResponseEntity.ok(data); } @@ -69,18 +67,19 @@ public class UIDataController { public ResponseEntity getLicensesData() { LicensesData data = new LicensesData(); Resource resource = new ClassPathResource("static/3rdPartyLicenses.json"); - + try { InputStream is = resource.getInputStream(); String json = new String(is.readAllBytes(), StandardCharsets.UTF_8); ObjectMapper mapper = new ObjectMapper(); - Map> licenseData = mapper.readValue(json, new TypeReference<>() {}); + Map> licenseData = + mapper.readValue(json, new TypeReference<>() {}); data.setDependencies(licenseData.get("dependencies")); } catch (IOException e) { log.error("Failed to load licenses data", e); data.setDependencies(Collections.emptyList()); } - + return ResponseEntity.ok(data); } @@ -90,25 +89,31 @@ public class UIDataController { PipelineData data = new PipelineData(); List pipelineConfigs = new ArrayList<>(); List> pipelineConfigsWithNames = new ArrayList<>(); - + if (new java.io.File(runtimePathConfig.getPipelineDefaultWebUiConfigs()).exists()) { - try (Stream paths = Files.walk(Paths.get(runtimePathConfig.getPipelineDefaultWebUiConfigs()))) { - List jsonFiles = paths.filter(Files::isRegularFile) - .filter(p -> p.toString().endsWith(".json")) - .toList(); - + try (Stream paths = + Files.walk(Paths.get(runtimePathConfig.getPipelineDefaultWebUiConfigs()))) { + List jsonFiles = + paths.filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith(".json")) + .toList(); + for (Path jsonFile : jsonFiles) { String content = Files.readString(jsonFile, StandardCharsets.UTF_8); pipelineConfigs.add(content); } - + for (String config : pipelineConfigs) { - Map jsonContent = new ObjectMapper() - .readValue(config, new TypeReference>() {}); + Map jsonContent = + new ObjectMapper() + .readValue(config, new TypeReference>() {}); String name = (String) jsonContent.get("name"); if (name == null || name.length() < 1) { - String filename = jsonFiles.get(pipelineConfigs.indexOf(config)) - .getFileName().toString(); + String filename = + jsonFiles + .get(pipelineConfigs.indexOf(config)) + .getFileName() + .toString(); name = filename.substring(0, filename.lastIndexOf('.')); } Map configWithName = new HashMap<>(); @@ -120,17 +125,17 @@ public class UIDataController { log.error("Failed to load pipeline configs", e); } } - + if (pipelineConfigsWithNames.isEmpty()) { Map configWithName = new HashMap<>(); configWithName.put("json", ""); configWithName.put("name", "No preloaded configs found"); pipelineConfigsWithNames.add(configWithName); } - + data.setPipelineConfigsWithNames(pipelineConfigsWithNames); data.setPipelineConfigs(pipelineConfigs); - + return ResponseEntity.ok(data); } @@ -141,26 +146,25 @@ public class UIDataController { if (userService != null) { username = userService.getCurrentUsername(); } - + List signatures = signatureService.getAvailableSignatures(username); List fonts = getFontNames(); - + SignData data = new SignData(); data.setSignatures(signatures); data.setFonts(fonts); - + return ResponseEntity.ok(data); } - @GetMapping("/ocr-pdf") @Operation(summary = "Get OCR PDF data") public ResponseEntity getOcrPdfData() { List languages = getAvailableTesseractLanguages(); - + OcrData data = new OcrData(); data.setLanguages(languages); - + return ResponseEntity.ok(data); } @@ -181,39 +185,49 @@ public class UIDataController { private List getFontNames() { List fontNames = new ArrayList<>(); fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2")); - fontNames.addAll(getFontNamesFromLocation( - "file:" + InstallationPathConfig.getStaticPath() + "fonts" + java.io.File.separator + "*")); + fontNames.addAll( + getFontNamesFromLocation( + "file:" + + InstallationPathConfig.getStaticPath() + + "fonts" + + java.io.File.separator + + "*")); return fontNames; } private List getFontNamesFromLocation(String locationPattern) { try { - Resource[] resources = GeneralUtils.getResourcesFromLocationPattern(locationPattern, resourceLoader); + Resource[] resources = + GeneralUtils.getResourcesFromLocationPattern(locationPattern, resourceLoader); return Arrays.stream(resources) - .map(resource -> { - try { - String filename = resource.getFilename(); - if (filename != null) { - int lastDotIndex = filename.lastIndexOf('.'); - if (lastDotIndex != -1) { - String name = filename.substring(0, lastDotIndex); - String extension = filename.substring(lastDotIndex + 1); - return new FontResource(name, extension); + .map( + resource -> { + try { + String filename = resource.getFilename(); + if (filename != null) { + int lastDotIndex = filename.lastIndexOf('.'); + if (lastDotIndex != -1) { + String name = filename.substring(0, lastDotIndex); + String extension = filename.substring(lastDotIndex + 1); + return new FontResource(name, extension); + } + } + return null; + } catch (Exception e) { + throw ExceptionUtils.createRuntimeException( + "error.fontLoadingFailed", + "Error processing font file", + e); } - } - return null; - } catch (Exception e) { - throw ExceptionUtils.createRuntimeException("error.fontLoadingFailed", "Error processing font file", e); - } - }) + }) .filter(Objects::nonNull) .toList(); } catch (Exception e) { - throw ExceptionUtils.createRuntimeException("error.fontDirectoryReadFailed", "Failed to read font directory", e); + throw ExceptionUtils.createRuntimeException( + "error.fontDirectoryReadFailed", "Failed to read font directory", e); } } - // Data classes @Data public static class HomeData { @@ -256,13 +270,19 @@ public class UIDataController { private static String getFormatFromExtension(String extension) { switch (extension) { - case "ttf": return "truetype"; - case "woff": return "woff"; - case "woff2": return "woff2"; - case "eot": return "embedded-opentype"; - case "svg": return "svg"; - default: return ""; + case "ttf": + return "truetype"; + case "woff": + return "woff"; + case "woff2": + return "woff2"; + case "eot": + return "embedded-opentype"; + case "svg": + return "svg"; + default: + return ""; } } } -} \ No newline at end of file +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/ProprietaryUIDataController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/ProprietaryUIDataController.java index 557d8ae85..f4eb114ef 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/ProprietaryUIDataController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/ProprietaryUIDataController.java @@ -1,9 +1,10 @@ package stirling.software.proprietary.controller.api; +import static stirling.software.common.util.ProviderUtils.validateProvider; + import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.*; -import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.ResponseEntity; @@ -14,7 +15,6 @@ import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.fasterxml.jackson.core.JsonProcessingException; @@ -24,7 +24,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.Data; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.ApplicationProperties; @@ -54,8 +53,6 @@ import stirling.software.proprietary.security.service.DatabaseService; import stirling.software.proprietary.security.service.TeamService; import stirling.software.proprietary.security.session.SessionPersistentRegistry; -import static stirling.software.common.util.ProviderUtils.validateProvider; - @Slf4j @RestController @RequestMapping("/api/v1/proprietary/ui-data") @@ -105,7 +102,7 @@ public class ProprietaryUIDataController { data.setRetentionDays(auditConfig.getRetentionDays()); data.setAuditLevels(AuditLevel.values()); data.setAuditEventTypes(AuditEventType.values()); - + return ResponseEntity.ok(data); } @@ -120,7 +117,8 @@ public class ProprietaryUIDataController { if (oauth != null && oauth.getEnabled()) { if (oauth.isSettingsValid()) { String firstChar = String.valueOf(oauth.getProvider().charAt(0)); - String clientName = oauth.getProvider().replaceFirst(firstChar, firstChar.toUpperCase()); + String clientName = + oauth.getProvider().replaceFirst(firstChar, firstChar.toUpperCase()); providerList.put("/oauth2/authorization/" + oauth.getProvider(), clientName); } @@ -128,40 +126,46 @@ public class ProprietaryUIDataController { if (client != null) { GoogleProvider google = client.getGoogle(); if (validateProvider(google)) { - providerList.put("/oauth2/authorization/" + google.getName(), google.getClientName()); + providerList.put( + "/oauth2/authorization/" + google.getName(), google.getClientName()); } GitHubProvider github = client.getGithub(); if (validateProvider(github)) { - providerList.put("/oauth2/authorization/" + github.getName(), github.getClientName()); + providerList.put( + "/oauth2/authorization/" + github.getName(), github.getClientName()); } KeycloakProvider keycloak = client.getKeycloak(); if (validateProvider(keycloak)) { - providerList.put("/oauth2/authorization/" + keycloak.getName(), keycloak.getClientName()); + providerList.put( + "/oauth2/authorization/" + keycloak.getName(), + keycloak.getClientName()); } } } SAML2 saml2 = securityProps.getSaml2(); - if (securityProps.isSaml2Active() && - applicationProperties.getSystem().getEnableAlphaFunctionality() && - applicationProperties.getPremium().isEnabled()) { + if (securityProps.isSaml2Active() + && applicationProperties.getSystem().getEnableAlphaFunctionality() + && applicationProperties.getPremium().isEnabled()) { String samlIdp = saml2.getProvider(); String saml2AuthenticationPath = "/saml2/authenticate/" + saml2.getRegistrationId(); - + if (!applicationProperties.getPremium().getProFeatures().isSsoAutoLogin()) { providerList.put(saml2AuthenticationPath, samlIdp + " (SAML 2)"); } } // Remove null entries - providerList.entrySet().removeIf(entry -> entry.getKey() == null || entry.getValue() == null); - + providerList + .entrySet() + .removeIf(entry -> entry.getKey() == null || entry.getValue() == null); + data.setProviderList(providerList); data.setLoginMethod(securityProps.getLoginMethod()); data.setAltLogin(!providerList.isEmpty() && securityProps.isAltLogin()); - + return ResponseEntity.ok(data); } @@ -172,12 +176,12 @@ public class ProprietaryUIDataController { List allUsers = userRepository.findAllWithTeam(); Iterator iterator = allUsers.iterator(); Map roleDetails = Role.getAllRoleDetails(); - + Map userSessions = new HashMap<>(); Map userLastRequest = new HashMap<>(); int activeUsers = 0; int disabledUsers = 0; - + while (iterator.hasNext()) { User user = iterator.next(); if (user != null) { @@ -193,7 +197,8 @@ public class ProprietaryUIDataController { } // Check if user is part of the Internal team - if (user.getTeam() != null && user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)) { + if (user.getTeam() != null + && user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)) { shouldRemove = true; } @@ -201,19 +206,23 @@ public class ProprietaryUIDataController { iterator.remove(); continue; } - + // Session status and last request time int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval(); boolean hasActiveSession = false; Date lastRequest = null; - Optional latestSession = sessionPersistentRegistry.findLatestSession(user.getUsername()); - + Optional latestSession = + sessionPersistentRegistry.findLatestSession(user.getUsername()); + if (latestSession.isPresent()) { SessionEntity sessionEntity = latestSession.get(); Date lastAccessedTime = sessionEntity.getLastRequest(); Instant now = Instant.now(); - Instant expirationTime = lastAccessedTime.toInstant().plus(maxInactiveInterval, ChronoUnit.SECONDS); - + Instant expirationTime = + lastAccessedTime + .toInstant() + .plus(maxInactiveInterval, ChronoUnit.SECONDS); + if (now.isAfter(expirationTime)) { sessionPersistentRegistry.expireSession(sessionEntity.getSessionId()); } else { @@ -223,33 +232,40 @@ public class ProprietaryUIDataController { } else { lastRequest = new Date(0); } - + userSessions.put(user.getUsername(), hasActiveSession); userLastRequest.put(user.getUsername(), lastRequest); - + if (hasActiveSession) activeUsers++; if (!user.isEnabled()) disabledUsers++; } } - - // Sort users by active status and last request date - List sortedUsers = allUsers.stream() - .sorted((u1, u2) -> { - boolean u1Active = userSessions.get(u1.getUsername()); - boolean u2Active = userSessions.get(u2.getUsername()); - if (u1Active && !u2Active) return -1; - if (!u1Active && u2Active) return 1; - - Date u1LastRequest = userLastRequest.getOrDefault(u1.getUsername(), new Date(0)); - Date u2LastRequest = userLastRequest.getOrDefault(u2.getUsername(), new Date(0)); - return u2LastRequest.compareTo(u1LastRequest); - }) - .toList(); - List allTeams = teamRepository.findAll().stream() - .filter(team -> !team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) - .toList(); - + // Sort users by active status and last request date + List sortedUsers = + allUsers.stream() + .sorted( + (u1, u2) -> { + boolean u1Active = userSessions.get(u1.getUsername()); + boolean u2Active = userSessions.get(u2.getUsername()); + if (u1Active && !u2Active) return -1; + if (!u1Active && u2Active) return 1; + + Date u1LastRequest = + userLastRequest.getOrDefault( + u1.getUsername(), new Date(0)); + Date u2LastRequest = + userLastRequest.getOrDefault( + u2.getUsername(), new Date(0)); + return u2LastRequest.compareTo(u1LastRequest); + }) + .toList(); + + List allTeams = + teamRepository.findAll().stream() + .filter(team -> !team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) + .toList(); + AdminSettingsData data = new AdminSettingsData(); data.setUsers(sortedUsers); data.setCurrentUsername(authentication.getName()); @@ -261,7 +277,7 @@ public class ProprietaryUIDataController { data.setDisabledUsers(disabledUsers); data.setTeams(allTeams); data.setMaxPaidUsers(applicationProperties.getPremium().getMaxUsers()); - + return ResponseEntity.ok(data); } @@ -272,7 +288,7 @@ public class ProprietaryUIDataController { if (authentication == null || !authentication.isAuthenticated()) { return ResponseEntity.status(401).build(); } - + Object principal = authentication.getPrincipal(); String username = null; boolean isOAuth2Login = false; @@ -287,16 +303,16 @@ public class ProprietaryUIDataController { username = saml2User.name(); isSaml2Login = true; } - + if (username == null) { return ResponseEntity.status(401).build(); } - + Optional user = userRepository.findByUsernameIgnoreCaseWithSettings(username); if (user.isEmpty()) { return ResponseEntity.status(404).build(); } - + String settingsJson; try { settingsJson = objectMapper.writeValueAsString(user.get().getSettings()); @@ -304,7 +320,7 @@ public class ProprietaryUIDataController { log.error("Error converting settings map", e); return ResponseEntity.status(500).build(); } - + AccountData data = new AccountData(); data.setUsername(username); data.setRole(user.get().getRolesAsString()); @@ -312,7 +328,7 @@ public class ProprietaryUIDataController { data.setChangeCredsFlag(user.get().isFirstLogin()); data.setOAuth2Login(isOAuth2Login); data.setSaml2Login(isSaml2Login); - + return ResponseEntity.ok(data); } @@ -321,9 +337,10 @@ public class ProprietaryUIDataController { @Operation(summary = "Get teams list data") public ResponseEntity getTeamsData() { List allTeamsWithCounts = teamRepository.findAllTeamsWithUserCount(); - List teamsWithCounts = allTeamsWithCounts.stream() - .filter(team -> !team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) - .toList(); + List teamsWithCounts = + allTeamsWithCounts.stream() + .filter(team -> !team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) + .toList(); List teamActivities = sessionRepository.findLatestActivityByTeam(); Map teamLastRequest = new HashMap<>(); @@ -332,11 +349,11 @@ public class ProprietaryUIDataController { Date lastActivity = (Date) result[1]; teamLastRequest.put(teamId, lastActivity); } - + TeamsData data = new TeamsData(); data.setTeamsWithCounts(teamsWithCounts); data.setTeamLastRequest(teamLastRequest); - + return ResponseEntity.ok(data); } @@ -344,8 +361,10 @@ public class ProprietaryUIDataController { @PreAuthorize("hasRole('ROLE_ADMIN')") @Operation(summary = "Get team details data") public ResponseEntity getTeamDetailsData(@PathVariable("id") Long id) { - Team team = teamRepository.findById(id) - .orElseThrow(() -> new RuntimeException("Team not found")); + Team team = + teamRepository + .findById(id) + .orElseThrow(() -> new RuntimeException("Team not found")); if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) { return ResponseEntity.status(403).build(); @@ -353,10 +372,19 @@ public class ProprietaryUIDataController { List teamUsers = userRepository.findAllByTeamId(id); List allUsers = userRepository.findAllWithTeam(); - List availableUsers = allUsers.stream() - .filter(user -> (user.getTeam() == null || !user.getTeam().getId().equals(id)) && - (user.getTeam() == null || !user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME))) - .toList(); + List availableUsers = + allUsers.stream() + .filter( + user -> + (user.getTeam() == null + || !user.getTeam().getId().equals(id)) + && (user.getTeam() == null + || !user.getTeam() + .getName() + .equals( + TeamService + .INTERNAL_TEAM_NAME))) + .toList(); List userSessions = sessionRepository.findLatestSessionByTeamId(id); Map userLastRequest = new HashMap<>(); @@ -365,13 +393,13 @@ public class ProprietaryUIDataController { Date lastRequest = (Date) result[1]; userLastRequest.put(username, lastRequest); } - + TeamDetailsData data = new TeamDetailsData(); data.setTeam(team); data.setTeamUsers(teamUsers); data.setAvailableUsers(availableUsers); data.setUserLastRequest(userLastRequest); - + return ResponseEntity.ok(data); } @@ -382,12 +410,12 @@ public class ProprietaryUIDataController { List backupList = databaseService.getBackupList(); String dbVersion = databaseService.getH2Version(); boolean isVersionUnknown = "Unknown".equalsIgnoreCase(dbVersion); - + DatabaseData data = new DatabaseData(); data.setBackupFiles(backupList); data.setDatabaseVersion(dbVersion); data.setVersionUnknown(isVersionUnknown); - + return ResponseEntity.ok(data); } @@ -453,4 +481,4 @@ public class ProprietaryUIDataController { private String databaseVersion; private boolean versionUnknown; } -} \ No newline at end of file +} diff --git a/docker/frontend/nginx.conf b/docker/frontend/nginx.conf index af4ca85f2..b1e5966fd 100644 --- a/docker/frontend/nginx.conf +++ b/docker/frontend/nginx.conf @@ -26,9 +26,9 @@ http { try_files $uri $uri/ /index.html; } - # Proxy API calls to backend - location /api/ { - proxy_pass ${VITE_API_BASE_URL}/api/; + # Proxy API calls to backend (with query parameters and sub-paths) + location ~ ^/api(.*)$ { + proxy_pass ${VITE_API_BASE_URL}/api$1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 081f746ee..37c0e5355 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -575,6 +575,10 @@ "title": "Validate PDF Signature", "desc": "Verify digital signatures and certificates in PDF documents" }, + "swagger": { + "title": "API Documentation", + "desc": "View API documentation and test endpoints" + }, "replaceColorPdf": { "title": "Advanced Colour options", "desc": "Replace colour for text and background in PDF and invert full colour of pdf to reduce file size" @@ -1521,6 +1525,12 @@ }, "note": "Release notes are only available in English" }, + "swagger": { + "title": "API Documentation", + "header": "API Documentation", + "desc": "View and test the Stirling PDF API endpoints", + "tags": "api,documentation,swagger,endpoints,development" + }, "cookieBanner": { "popUp": { "title": "How we use Cookies", diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index e73175694..1b258a824 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -575,6 +575,10 @@ "title": "Validate PDF Signature", "desc": "Verify digital signatures and certificates in PDF documents" }, + "swagger": { + "title": "API Documentation", + "desc": "View API documentation and test endpoints" + }, "replaceColorPdf": { "title": "Replace and Invert Color", "desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size" @@ -1521,6 +1525,12 @@ }, "note": "Release notes are only available in English" }, + "swagger": { + "title": "API Documentation", + "header": "API Documentation", + "desc": "View and test the Stirling PDF API endpoints", + "tags": "api,documentation,swagger,endpoints,development" + }, "cookieBanner": { "popUp": { "title": "How we use Cookies", diff --git a/frontend/src/hooks/useToolManagement.tsx b/frontend/src/hooks/useToolManagement.tsx index 7ada59024..bdf2744ac 100644 --- a/frontend/src/hooks/useToolManagement.tsx +++ b/frontend/src/hooks/useToolManagement.tsx @@ -2,6 +2,7 @@ import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import ContentCutIcon from "@mui/icons-material/ContentCut"; import ZoomInMapIcon from "@mui/icons-material/ZoomInMap"; +import ApiIcon from "@mui/icons-material/Api"; import { useMultipleEndpointsEnabled } from "./useEndpointConfig"; import { Tool, ToolDefinition, BaseToolProps, ToolRegistry } from "../types/tool"; @@ -26,6 +27,15 @@ const toolDefinitions: Record = { description: "Reduce PDF file size", endpoints: ["compress-pdf"] }, + swagger: { + id: "swagger", + icon: , + component: React.lazy(() => import("../tools/SwaggerUI")), + maxFiles: 0, + category: "utility", + description: "Open API documentation", + endpoints: ["swagger-ui"] + }, }; diff --git a/frontend/src/tools/SwaggerUI.tsx b/frontend/src/tools/SwaggerUI.tsx new file mode 100644 index 000000000..0712b6068 --- /dev/null +++ b/frontend/src/tools/SwaggerUI.tsx @@ -0,0 +1,18 @@ +import React, { useEffect } from 'react'; +import { BaseToolProps } from '../types/tool'; + +const SwaggerUI: React.FC = () => { + useEffect(() => { + // Redirect to Swagger UI + window.open('/swagger-ui/5.21.0/index.html', '_blank'); + }, []); + + return ( +
+

Opening Swagger UI in a new tab...

+

If it didn't open automatically, click here

+
+ ); +}; + +export default SwaggerUI; \ No newline at end of file