refactor: use react router

This commit is contained in:
ReaJason
2025-04-06 13:55:17 +08:00
parent cd231c5af7
commit fa206e8acc
12 changed files with 151 additions and 212 deletions
@@ -1,11 +1,9 @@
package com.reajason.javaweb.boot.controller;
import com.reajason.javaweb.boot.entity.Config;
import com.reajason.javaweb.memshell.Packers;
import com.reajason.javaweb.memshell.Server;
import com.reajason.javaweb.memshell.ShellTool;
import com.reajason.javaweb.memshell.server.AbstractShell;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -20,8 +18,28 @@ import java.util.*;
@RequestMapping("/config")
@CrossOrigin("*")
public class ConfigController {
@RequestMapping("/servers")
public Map<String, List<String>> getServers() {
Map<String, List<String>> servers = new LinkedHashMap<>();
for (Server server : Server.values()) {
if (server.getShell() != null) {
Set<String> supportedShellTypes = server.getShell().getShellInjectorMapping().getSupportedShellTypes();
servers.put(server.name(), supportedShellTypes.stream().toList());
}
}
return servers;
}
@RequestMapping("/packers")
public List<String> getPackers() {
return Arrays.stream(Packers.values())
.filter(packers -> packers.getParentPacker() == null)
.map(Packers::name).toList();
}
@RequestMapping
public ResponseEntity<?> config() {
public Map<String, Map<?, ?>> config() {
Map<String, Map<?, ?>> coreMap = new HashMap<>(16);
for (Server value : Server.values()) {
AbstractShell shell = value.getShell();
@@ -38,21 +56,6 @@ public class ConfigController {
}
coreMap.put(value.name(), map);
}
Config config = new Config();
Map<String, List<String>> servers = new LinkedHashMap<>();
for (Server server : Server.values()) {
if (server.getShell() != null) {
Set<String> supportedShellTypes = server.getShell().getShellInjectorMapping().getSupportedShellTypes();
servers.put(server.name(), supportedShellTypes.stream().toList());
}
}
config.setServers(servers);
config.setCore(coreMap);
config.setPackers(
Arrays.stream(Packers.values())
.filter(packers -> packers.getParentPacker() == null)
.map(Packers::name).toList()
);
return ResponseEntity.ok(config);
return coreMap;
}
}
@@ -1,8 +1,8 @@
package com.reajason.javaweb.boot.controller;
import com.reajason.javaweb.memshell.MemShellGenerator;
import com.reajason.javaweb.boot.dto.GenerateRequest;
import com.reajason.javaweb.boot.dto.GenerateResponse;
import com.reajason.javaweb.memshell.MemShellGenerator;
import com.reajason.javaweb.memshell.config.GenerateResult;
import com.reajason.javaweb.memshell.config.InjectorConfig;
import com.reajason.javaweb.memshell.config.ShellConfig;
@@ -10,7 +10,6 @@ import com.reajason.javaweb.memshell.config.ShellToolConfig;
import com.reajason.javaweb.memshell.packer.AggregatePacker;
import com.reajason.javaweb.memshell.packer.Packer;
import com.reajason.javaweb.memshell.packer.jar.JarPacker;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Base64;
@@ -24,18 +23,18 @@ import java.util.Base64;
@CrossOrigin("*")
public class GeneratorController {
@PostMapping
public ResponseEntity<?> generate(@RequestBody GenerateRequest request) {
public GenerateResponse generate(@RequestBody GenerateRequest request) {
ShellConfig shellConfig = request.getShellConfig();
ShellToolConfig shellToolConfig = request.parseShellToolConfig();
InjectorConfig injectorConfig = request.getInjectorConfig();
GenerateResult generateResult = MemShellGenerator.generate(shellConfig, injectorConfig, shellToolConfig);
Packer packer = request.getPacker().getInstance();
if (packer instanceof JarPacker) {
return ResponseEntity.ok(new GenerateResponse(generateResult, Base64.getEncoder().encodeToString(((JarPacker) packer).packBytes(generateResult))));
return new GenerateResponse(generateResult, Base64.getEncoder().encodeToString(((JarPacker) packer).packBytes(generateResult)));
} else if (packer instanceof AggregatePacker) {
return ResponseEntity.ok(new GenerateResponse(generateResult, ((AggregatePacker) packer).packAll(generateResult)));
return new GenerateResponse(generateResult, ((AggregatePacker) packer).packAll(generateResult));
} else {
return ResponseEntity.ok(new GenerateResponse(generateResult, packer.pack(generateResult)));
return new GenerateResponse(generateResult, packer.pack(generateResult));
}
}
}
BIN
View File
Binary file not shown.
+9 -12
View File
@@ -15,17 +15,16 @@
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@tanstack/router-plugin": "^1.114.32",
"@types/node": "^22.13.17",
"@types/node": "^22.14.0",
"@types/react": "^19.1.0",
"@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^19.1.1",
"@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react": "^4.3.4",
"rimraf": "^6.0.1",
"tailwindcss": "^4.1.1",
"typescript": "^5.8.2",
"vite": "^6.2.4",
"tailwindcss": "^4.1.3",
"typescript": "^5.8.3",
"vite": "^6.2.5",
"vite-bundle-visualizer": "^1.2.1"
},
"dependencies": {
@@ -56,19 +55,15 @@
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@tailwindcss/vite": "^4.1.1",
"@tanstack/react-query": "^5.71.3",
"@tanstack/react-router": "^1.114.32",
"@tanstack/router-devtools": "^1.114.32",
"@tailwindcss/vite": "^4.1.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.5.2",
"embla-carousel-react": "^8.6.0",
"i18next": "^24.2.3",
"input-otp": "^1.4.2",
"lucide-react": "^0.487.0",
"next-themes": "^0.4.6",
"react": "^19.1.0",
"react-copy-to-clipboard": "^5.1.0",
"react-day-picker": "9.6.4",
@@ -76,8 +71,10 @@
"react-hook-form": "^7.55.0",
"react-i18next": "^15.4.1",
"react-resizable-panels": "^2.1.7",
"react-router": "^7.5.0",
"react-router-dom": "^7.5.0",
"react-syntax-highlighter": "^15.6.1",
"recharts": "^2.15.1",
"recharts": "^2.15.2",
"sonner": "^2.0.3",
"tailwind-merge": "^3.1.0",
"tailwind-scrollbar": "^4.0.2",
@@ -1,17 +1,14 @@
import { LanguageSwitcher } from "@/components/language-switcher";
import { ModeToggle } from "@/components/mode-toggle.tsx";
import { ThemeProvider } from "@/components/theme-provider.tsx";
import { Button } from "@/components/ui/button.tsx";
import { Toaster } from "@/components/ui/sonner";
import VersionBadge from "@/components/version-badge";
import { GitHubIcon } from "@/icon";
import { Outlet, createRootRoute } from "@tanstack/react-router";
import { Button } from "@/components/ui/button";
import { Outlet } from "react-router-dom";
export const Route = createRootRoute({
component: RootComponent,
});
function RootComponent() {
export default function RootLayout() {
return (
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
<Toaster />
+5 -19
View File
@@ -1,37 +1,23 @@
import { RouterProvider, createRouter } from "@tanstack/react-router";
import ReactDOM from "react-dom/client";
import { routeTree } from "./routeTree.gen";
import "./index.css";
import { TailwindIndicator } from "@/components/tailwind-indicator.tsx";
import { env } from "@/config.ts";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { I18nextProvider } from "react-i18next";
import i18n from "./i18n/i18n";
const queryClient = new QueryClient();
const router = createRouter({
routeTree,
basepath: env.BASE_PATH,
defaultPreload: "intent",
});
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
import { RouterProvider } from "react-router-dom";
import { router } from "./router";
import { QueryProvider } from "./providers/query-client-provider";
const rootElement = document.getElementById("app") as HTMLElement;
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<QueryClientProvider client={queryClient}>
<QueryProvider>
<I18nextProvider i18n={i18n}>
<RouterProvider router={router} />
{env.MODE !== "production" && <TailwindIndicator />}
</I18nextProvider>
</QueryClientProvider>,
</QueryProvider>
);
}
@@ -10,41 +10,56 @@ import {
ConfigResponseType,
GenerateResponse,
GenerateResult,
MainConfig,
PackerConfig,
ServerConfig,
ShellToolType,
} from "@/types/shell.ts";
import { transformToPostData } from "@/utils/transformer.ts";
import { customValidation, transformToPostData } from "@/utils/transformer.ts";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { LoaderCircle, WandSparklesIcon } from "lucide-react";
import { useState, useTransition } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { useQuery } from "@tanstack/react-query";
export const Route = createFileRoute("/")({
component: IndexComponent,
});
export default function IndexPage() {
const { data: serverConfig } = useQuery<ServerConfig>({
queryKey: ["serverConfig"],
queryFn: async () => {
const response = await fetch(`${env.API_URL}/config/servers`);
return await response.json();
},
});
function IndexComponent() {
const { data } = useQuery<ConfigResponseType>({
queryKey: ["config"],
const { data: mainConfig } = useQuery<MainConfig>({
queryKey: ["mainConfig"],
queryFn: async () => {
const response = await fetch(`${env.API_URL}/config`);
return await response.json();
},
});
const { data: packerConfig } = useQuery<PackerConfig>({
queryKey: ["packerConfig"],
queryFn: async () => {
const response = await fetch(`${env.API_URL}/config/packers`);
return await response.json();
},
});
const { t } = useTranslation();
const form = useForm<FormSchema>({
resolver: zodResolver(formSchema),
defaultValues: {
server: "",
server: "Tomcat",
targetJdkVersion: "50",
debug: false,
bypassJavaModule: false,
shellClassName: "",
shellTool: "Godzilla",
shellType: "",
shellType: "Listener",
urlPattern: "/*",
godzillaPass: "pass",
godzillaKey: "key",
@@ -64,40 +79,11 @@ function IndexComponent() {
const [packMethod, setPackMethod] = useState<string>("");
const [isActionPending, startTransition] = useTransition();
function customValidation(values: FormSchema) {
if (values.shellType.endsWith("Servlet") && (values.urlPattern === "/*" || !values.urlPattern)) {
toast.warning(t("tips.servletUrlPattern"));
return false;
}
if (values.shellType.endsWith("ControllerHandler") && (values.urlPattern === "/*" || !values.urlPattern)) {
toast.warning(t("tips.controllerUrlPattern"));
return false;
}
if (
(values.shellType === "HandlerMethod" || values.shellType === "HandlerFunction") &&
(values.urlPattern === "/*" || !values.urlPattern)
) {
toast.warning(t("tips.handlerUrlPattern"));
return false;
}
if (values.shellTool === ShellToolType.Custom && !values.shellClassBase64) {
toast.warning(t("tips.customShellClass"));
return false;
}
return true;
}
async function onSubmit(values: FormSchema) {
const onSubmit = async (data: FormSchema) => {
startTransition(async () => {
if (!customValidation(values)) {
return;
}
const postData = transformToPostData(values);
try {
customValidation(t, data);
const postData = transformToPostData(data);
const response = await fetch(`${env.API_URL}/generate`, {
method: "POST",
headers: {
@@ -105,31 +91,30 @@ function IndexComponent() {
},
body: JSON.stringify(postData),
});
await new Promise((resolve) => setTimeout(resolve, 200));
if (response.ok) {
const json: GenerateResponse = await response.json();
setPackResult(json.packResult);
setAllPackResults(json.allPackResults);
setGenerateResult(json.generateResult);
setPackMethod(values.packingMethod);
toast.success(t("success.generated"));
} else {
if (!response.ok) {
const json: APIErrorResponse = await response.json();
toast.error(t("errors.generationFailed", { error: json.error }));
}
} catch (err) {
const error = err as Error;
toast.error(t("errors.generationFailed", { error: error.message }));
const result = (await response.json()) as GenerateResponse;
setGenerateResult(result.generateResult);
setPackResult(result.packResult);
setAllPackResults(result.allPackResults);
setPackMethod(data.packingMethod);
toast.success(t("success.generated"));
} catch (error) {
toast.error(t("errors.generationFailed", { error: (error as Error).message }));
}
});
}
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col xl:flex-row gap-4 p-4">
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col xl:flex-row gap-4 p-4">
<div className="w-full xl:w-1/2 space-y-4">
<MainConfigCard servers={data?.servers} mainConfig={data?.core} form={form} />
<PackageConfigCard packerConfig={data?.packers} form={form} />
<MainConfigCard servers={serverConfig} mainConfig={mainConfig} form={form} />
<PackageConfigCard packerConfig={packerConfig} form={form} />
<Button className="w-full" type="submit" disabled={isActionPending}>
{isActionPending ? <LoaderCircle className="animate-spin" /> : <WandSparklesIcon />}
{t("buttons.generate")}
@@ -0,0 +1,15 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactNode } from "react";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
retry: 1,
},
},
});
export function QueryProvider({ children }: { children: ReactNode }) {
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}
-86
View File
@@ -1,86 +0,0 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
// Import Routes
import { Route as rootRoute } from "./routes/__root";
import { Route as IndexImport } from "./routes/index";
// Create/Update Routes
const IndexRoute = IndexImport.update({
id: "/",
path: "/",
getParentRoute: () => rootRoute,
} as any);
// Populate the FileRoutesByPath interface
declare module "@tanstack/react-router" {
interface FileRoutesByPath {
"/": {
id: "/";
path: "/";
fullPath: "/";
preLoaderRoute: typeof IndexImport;
parentRoute: typeof rootRoute;
};
}
}
// Create and export the route tree
export interface FileRoutesByFullPath {
"/": typeof IndexRoute;
}
export interface FileRoutesByTo {
"/": typeof IndexRoute;
}
export interface FileRoutesById {
__root__: typeof rootRoute;
"/": typeof IndexRoute;
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath;
fullPaths: "/";
fileRoutesByTo: FileRoutesByTo;
to: "/";
id: "__root__" | "/";
fileRoutesById: FileRoutesById;
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute;
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
};
export const routeTree = rootRoute._addFileChildren(rootRouteChildren)._addFileTypes<FileRouteTypes>();
/* ROUTE_MANIFEST_START
{
"routes": {
"__root__": {
"filePath": "__root.tsx",
"children": [
"/"
]
},
"/": {
"filePath": "index.tsx"
}
}
}
ROUTE_MANIFEST_END */
+22
View File
@@ -0,0 +1,22 @@
import { createBrowserRouter } from "react-router-dom";
import { env } from "@/config";
import RootLayout from "@/components/layouts/root-layout";
import IndexPage from "@/pages";
export const router = createBrowserRouter(
[
{
path: "/",
element: <RootLayout />,
children: [
{
index: true,
element: <IndexPage />,
},
],
},
],
{
basename: env.BASE_PATH,
}
);
+23 -1
View File
@@ -1,5 +1,27 @@
import { FormSchema } from "@/types/schema.ts";
import { InjectorConfig, ShellConfig, ShellToolConfig } from "@/types/shell.ts";
import { InjectorConfig, ShellConfig, ShellToolConfig, ShellToolType } from "@/types/shell.ts";
import { TFunction } from "i18next";
export function customValidation(t: TFunction<"translation", undefined>, values: FormSchema) {
if (values.shellType.endsWith("Servlet") && (values.urlPattern === "/*" || !values.urlPattern)) {
throw new Error(t("tips.servletUrlPattern"));
}
if (values.shellType.endsWith("ControllerHandler") && (values.urlPattern === "/*" || !values.urlPattern)) {
throw new Error(t("tips.controllerUrlPattern"));
}
if (
(values.shellType === "HandlerMethod" || values.shellType === "HandlerFunction") &&
(values.urlPattern === "/*" || !values.urlPattern)
) {
throw new Error(t("tips.handlerUrlPattern"));
}
if (values.shellTool === ShellToolType.Custom && !values.shellClassBase64) {
throw new Error(t("tips.customShellClass"));
}
}
export function transformToPostData(formValue: FormSchema) {
const shellConfig: ShellConfig = {
+1 -2
View File
@@ -1,12 +1,11 @@
import path from "node:path";
import tailwindcss from "@tailwindcss/vite";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [TanStackRouterVite({ autoCodeSplitting: true }), react(), tailwindcss()],
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),