add send cmd page
This commit is contained in:
parent
328d499dca
commit
8f41579972
90
package-lock.json
generated
90
package-lock.json
generated
|
@ -13,7 +13,7 @@
|
|||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/react-form": "^1.15.0",
|
||||
"@tanstack/react-form": "^1.23.0",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@tanstack/react-router": "^1.121.2",
|
||||
"@tanstack/react-router-devtools": "^1.121.2",
|
||||
|
@ -31,6 +31,7 @@
|
|||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"zod": "^3.25.76",
|
||||
"zustand": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -2385,12 +2386,26 @@
|
|||
"vite": "^5.2.0 || ^6 || ^7"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/devtools-event-client": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/devtools-event-client/-/devtools-event-client-0.2.5.tgz",
|
||||
"integrity": "sha512-iVdqw879KETXyyPHc3gQR5Ld0GjlPLk7bKenBUhzr3+z1FiQZvsbfgYfRRokTSPcgwANAV7aA2Uv05nx5xWT8A==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/form-core": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.15.0.tgz",
|
||||
"integrity": "sha512-zMNyxb/J/JnFmW4Gzb1TSxaXmwNhvsaF9p3dGRpE93TMGp2ojPKK7V5LZ43ZV7iFTYWTL8NOIU8ZXuf9qZVkmw==",
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.23.0.tgz",
|
||||
"integrity": "sha512-aBJnO5iA1TtMK/SSpAvd/y6cVsBgbu5Br2FftvW6r7TkHCcZ/lB2DMx3G2dk6SG7sW0g8s6F7iaMjrvqBGooDw==",
|
||||
"dependencies": {
|
||||
"@tanstack/store": "^0.7.2"
|
||||
"@tanstack/devtools-event-client": "^0.2.5",
|
||||
"@tanstack/store": "^0.7.5",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
|
@ -2419,30 +2434,26 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-form": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-form/-/react-form-1.15.0.tgz",
|
||||
"integrity": "sha512-bAawFDxR1wLn+eXli6MSyS4Nw0vTyHuW3CybjZGtk7NIcYxyaAm9cW/jPUX2j/KMDtjVNg5RMpfmufxDrsNHyA==",
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-form/-/react-form-1.23.0.tgz",
|
||||
"integrity": "sha512-wZ/cBjqj5dgWG79TQOsilbs8WzdouJj3iXw1DAZvQ4nejobsTGl0huU7SZZ0mnFtdNHbre1bFSdWyw+GqFcIhQ==",
|
||||
"dependencies": {
|
||||
"@tanstack/form-core": "1.15.0",
|
||||
"@tanstack/react-store": "^0.7.3",
|
||||
"@tanstack/form-core": "1.23.0",
|
||||
"@tanstack/react-store": "^0.7.5",
|
||||
"decode-formdata": "^0.9.0",
|
||||
"devalue": "^5.1.1"
|
||||
"devalue": "^5.3.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/react-start": "^1.112.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"vinxi": "^0.5.0"
|
||||
"@tanstack/react-start": "^1.130.10",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@tanstack/react-start": {
|
||||
"optional": true
|
||||
},
|
||||
"vinxi": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -2506,11 +2517,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-store": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.7.3.tgz",
|
||||
"integrity": "sha512-3Dnqtbw9P2P0gw8uUM8WP2fFfg8XMDSZCTsywRPZe/XqqYW8PGkXKZTvP0AHkE4mpqP9Y43GpOg9vwO44azu6Q==",
|
||||
"version": "0.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.7.7.tgz",
|
||||
"integrity": "sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg==",
|
||||
"dependencies": {
|
||||
"@tanstack/store": "0.7.2",
|
||||
"@tanstack/store": "0.7.7",
|
||||
"use-sync-external-store": "^1.5.0"
|
||||
},
|
||||
"funding": {
|
||||
|
@ -2685,9 +2696,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tanstack/store": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.2.tgz",
|
||||
"integrity": "sha512-RP80Z30BYiPX2Pyo0Nyw4s1SJFH2jyM6f9i3HfX4pA+gm5jsnYryscdq2aIQLnL4TaGuQMO+zXmN9nh1Qck+Pg==",
|
||||
"version": "0.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.7.tgz",
|
||||
"integrity": "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
|
@ -3215,9 +3226,9 @@
|
|||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
|
||||
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
|
@ -3897,9 +3908,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
|
||||
"integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.3.2.tgz",
|
||||
"integrity": "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw=="
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "8.0.2",
|
||||
|
@ -7232,6 +7243,18 @@
|
|||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
||||
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"bin": {
|
||||
"uuid": "dist-node/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
|
@ -7241,10 +7264,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"license": "MIT",
|
||||
"version": "6.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
|
||||
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/react-form": "^1.15.0",
|
||||
"@tanstack/react-form": "^1.23.0",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@tanstack/react-router": "^1.121.2",
|
||||
"@tanstack/react-router-devtools": "^1.121.2",
|
||||
|
@ -35,6 +35,7 @@
|
|||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"zod": "^3.25.76",
|
||||
"zustand": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
90
src/components/command-form.tsx
Normal file
90
src/components/command-form.tsx
Normal file
|
@ -0,0 +1,90 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useForm } from "@tanstack/react-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Loader2, Terminal, AlertTriangle, CheckCircle } from "lucide-react";
|
||||
|
||||
interface ShellCommandFormProps {
|
||||
onExecute: (command: string) => Promise<{ success: boolean; output: string }>;
|
||||
}
|
||||
|
||||
export function ShellCommandForm({ onExecute }: ShellCommandFormProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// init form
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
command: "",
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await onExecute(value.command);
|
||||
if (res.success) {
|
||||
form.reset();
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Form */}
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
className="space-y-5"
|
||||
>
|
||||
{/* Field: command */}
|
||||
<form.Field
|
||||
name="command"
|
||||
validators={{
|
||||
onChange: z
|
||||
.string()
|
||||
.min(1, "Nhập command để thực thi")
|
||||
.max(500, "Command quá dài"),
|
||||
}}
|
||||
children={(field) => (
|
||||
<div className="w-full px-0">
|
||||
<Textarea
|
||||
className="w-full h-[25vh]"
|
||||
placeholder="Nhập lệnh..."
|
||||
value={field.state.value}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{field.state.meta.errors?.length > 0 && (
|
||||
<p className="text-sm text-red-500">
|
||||
{field.state.meta.errors.join(", ")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Yêu cầu thiết bị thực thi
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
66
src/components/ui/alert.tsx
Normal file
66
src/components/ui/alert.tsx
Normal file
|
@ -0,0 +1,66 @@
|
|||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
46
src/components/ui/badge.tsx
Normal file
46
src/components/ui/badge.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
|
@ -1,36 +1,36 @@
|
|||
import { useState } from "react";
|
||||
import {
|
||||
Dialog, DialogContent, DialogDescription, DialogFooter,
|
||||
DialogHeader, DialogTitle, DialogTrigger,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Plus } from "lucide-react";
|
||||
import { formOptions, useForm } from "@tanstack/react-form";
|
||||
import { useState } from "react";
|
||||
import { useForm, formOptions } from "@tanstack/react-form";
|
||||
import { toast } from "sonner";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
|
||||
interface UploadDialogProps {
|
||||
onSubmit: (formData: FormData) => Promise<void>;
|
||||
accept?: string;
|
||||
onSubmit: (
|
||||
fd: FormData,
|
||||
config?: { onUploadProgress?: (e: AxiosProgressEvent) => void }
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
interface UploadFormProps {
|
||||
files: FileList;
|
||||
newVersion: string;
|
||||
}
|
||||
const formOpts = formOptions({
|
||||
defaultValues: { files: new DataTransfer().files, newVersion: "" },
|
||||
});
|
||||
|
||||
const defaultInput: UploadFormProps = {
|
||||
files: new DataTransfer().files,
|
||||
newVersion: "",
|
||||
};
|
||||
|
||||
const formOpts = formOptions({ defaultValues: defaultInput });
|
||||
|
||||
export function UploadDialog({ onSubmit, accept = ".exe,.msi,.apk" }: UploadDialogProps) {
|
||||
export function UploadDialog({ onSubmit }: UploadDialogProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [uploadPercent, setUploadPercent] = useState(0);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [isDone, setIsDone] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
...formOpts,
|
||||
|
@ -40,39 +40,71 @@ export function UploadDialog({ onSubmit, accept = ".exe,.msi,.apk" }: UploadDial
|
|||
return;
|
||||
}
|
||||
|
||||
const fd = new FormData();
|
||||
Array.from(value.files).forEach((file) => fd.append("files", file));
|
||||
fd.append("newVersion", value.newVersion);
|
||||
|
||||
try {
|
||||
setIsUploading(true);
|
||||
setUploadPercent(0);
|
||||
await onSubmit(fd);
|
||||
setIsOpen(false);
|
||||
form.reset();
|
||||
setIsDone(false);
|
||||
|
||||
const fd = new FormData();
|
||||
Array.from(value.files).forEach((f) => fd.append("FileInput", f));
|
||||
fd.append("Version", value.newVersion);
|
||||
|
||||
await onSubmit(fd, {
|
||||
onUploadProgress: (e: AxiosProgressEvent) => {
|
||||
if (e.total) {
|
||||
const progress = Math.round((e.loaded * 100) / e.total);
|
||||
setUploadPercent(progress);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
setIsDone(true);
|
||||
} catch (error) {
|
||||
console.error("Upload error:", error);
|
||||
toast.error("Upload thất bại!");
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleDialogClose = (open: boolean) => {
|
||||
if (isUploading) return;
|
||||
setIsOpen(open);
|
||||
if (!open) {
|
||||
setUploadPercent(0);
|
||||
setIsDone(false);
|
||||
form.reset();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<Dialog open={isOpen} onOpenChange={handleDialogClose}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2">
|
||||
<Plus className="h-4 w-4" /> Cập nhật phiên bản mới
|
||||
</Button>
|
||||
<Button>Tải lên phiên bản mới</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Cập nhật phiên bản mới</DialogTitle>
|
||||
<DialogDescription>Chọn tệp và nhập số phiên bản</DialogDescription>
|
||||
<DialogTitle>Cập nhật phiên bản</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form className="space-y-4" onSubmit={form.handleSubmit}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<form.Field name="newVersion">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<Label>Phiên bản</Label>
|
||||
<Input
|
||||
value={field.state.value}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
placeholder="e.g., 1.0.0"
|
||||
placeholder="1.0.0"
|
||||
disabled={isUploading || isDone}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -80,28 +112,50 @@ export function UploadDialog({ onSubmit, accept = ".exe,.msi,.apk" }: UploadDial
|
|||
|
||||
<form.Field name="files">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<Label>File ứng dụng</Label>
|
||||
<div>
|
||||
<Label>File</Label>
|
||||
<Input
|
||||
type="file"
|
||||
accept={accept}
|
||||
onChange={(e) => e.target.files && field.handleChange(e.target.files)}
|
||||
accept=".exe,.msi,.apk"
|
||||
onChange={(e) =>
|
||||
e.target.files && field.handleChange(e.target.files)
|
||||
}
|
||||
disabled={isUploading || isDone}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</form.Field>
|
||||
|
||||
{uploadPercent > 0 && (
|
||||
{(uploadPercent > 0 || isUploading || isDone) && (
|
||||
<div className="space-y-2">
|
||||
<Label>Tiến trình upload</Label>
|
||||
<Progress value={uploadPercent} className="w-full" />
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>{isDone ? "Hoàn tất!" : "Đang tải lên..."}</span>
|
||||
<span>{uploadPercent}%</span>
|
||||
</div>
|
||||
<Progress value={uploadPercent} className="w-full" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => setIsOpen(false)}>Hủy</Button>
|
||||
<Button type="submit">Tải lên</Button>
|
||||
{!isDone ? (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => handleDialogClose(false)}
|
||||
disabled={isUploading}
|
||||
>
|
||||
Hủy
|
||||
</Button>
|
||||
<Button type="submit" disabled={isUploading}>
|
||||
{isUploading ? "Đang tải..." : "Upload"}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button type="button" onClick={() => handleDialogClose(false)}>
|
||||
Hoàn tất
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
|
|
|
@ -16,6 +16,8 @@ export const API_ENDPOINTS = {
|
|||
GET_DEVICE_FROM_ROOM: (roomName: string) =>
|
||||
`${BASE_URL}/DeviceComm/room/${roomName}`,
|
||||
UPDATE_AGENT: `${BASE_URL}/DeviceComm/updateagent`,
|
||||
SEND_COMMAND: `${BASE_URL}/DeviceComm/shellcommand`,
|
||||
CHANGE_DEVICE_ROOM: `${BASE_URL}/DeviceComm/changeroom`,
|
||||
},
|
||||
SSE_EVENTS: {
|
||||
DEVICE_ONLINE: `${BASE_URL}/Sse/events/onlineDevices`,
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import axios, { type Method } from "axios"
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import axios, { type Method } from "axios";
|
||||
|
||||
type MutationDataOptions<TInput, TOutput> = {
|
||||
url: string
|
||||
method?: Method // POST, PUT, PATCH, DELETE
|
||||
onSuccess?: (data: TOutput) => void
|
||||
onError?: (error: any) => void
|
||||
config?: {
|
||||
onUploadProgress?: (e: ProgressEvent) => void
|
||||
}
|
||||
invalidate?: string[][] // List of queryKeys to invalidate
|
||||
}
|
||||
url: string;
|
||||
method?: Method;
|
||||
onSuccess?: (data: TOutput) => void;
|
||||
onError?: (error: any) => void;
|
||||
invalidate?: string[][];
|
||||
};
|
||||
|
||||
export function useMutationData<TInput = any, TOutput = any>({
|
||||
url,
|
||||
|
@ -19,24 +16,31 @@ export function useMutationData<TInput = any, TOutput = any>({
|
|||
onError,
|
||||
invalidate = [],
|
||||
}: MutationDataOptions<TInput, TOutput>) {
|
||||
const queryClient = useQueryClient()
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<TOutput, any, { data: TInput; config?: any }>({
|
||||
mutationFn: async ({ data, config }) => {
|
||||
const isFormData = data instanceof FormData;
|
||||
|
||||
return useMutation<TOutput, any, TInput>({
|
||||
mutationFn: async (data: TInput) => {
|
||||
const response = await axios.request({
|
||||
url,
|
||||
method,
|
||||
data,
|
||||
})
|
||||
return response.data
|
||||
headers: {
|
||||
...(isFormData
|
||||
? {}
|
||||
: { "Content-Type": "application/json" }),
|
||||
},
|
||||
...config,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
invalidate.forEach((key) =>
|
||||
queryClient.invalidateQueries({ queryKey: key })
|
||||
)
|
||||
onSuccess?.(data)
|
||||
);
|
||||
onSuccess?.(data);
|
||||
},
|
||||
onError,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
SidebarInset,
|
||||
SidebarTrigger,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { Home, Building, AppWindow } from "lucide-react";
|
||||
import { Home, Building, AppWindow, Terminal } from "lucide-react";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { API_ENDPOINTS, BASE_URL } from "@/config/api";
|
||||
|
@ -67,6 +67,7 @@ export default function AppLayout({ children }: AppLayoutProps) {
|
|||
{ title: "Quản lý phần mềm", to: "/apps", icon: AppWindow,
|
||||
onPointerEnter: handlePrefetchSofware,
|
||||
},
|
||||
{ title: "Gửi lệnh CMD", to: "/command", icon: Terminal },
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
|
@ -13,6 +13,7 @@ import { Route as AuthenticatedRouteImport } from './routes/_authenticated'
|
|||
import { Route as AuthRouteImport } from './routes/_auth'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as AuthenticatedRoomIndexRouteImport } from './routes/_authenticated/room/index'
|
||||
import { Route as AuthenticatedCommandIndexRouteImport } from './routes/_authenticated/command/index'
|
||||
import { Route as AuthenticatedAppsIndexRouteImport } from './routes/_authenticated/apps/index'
|
||||
import { Route as AuthenticatedAgentIndexRouteImport } from './routes/_authenticated/agent/index'
|
||||
import { Route as AuthLoginIndexRouteImport } from './routes/_auth/login/index'
|
||||
|
@ -36,6 +37,12 @@ const AuthenticatedRoomIndexRoute = AuthenticatedRoomIndexRouteImport.update({
|
|||
path: '/room/',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedCommandIndexRoute =
|
||||
AuthenticatedCommandIndexRouteImport.update({
|
||||
id: '/command/',
|
||||
path: '/command/',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedAppsIndexRoute = AuthenticatedAppsIndexRouteImport.update({
|
||||
id: '/apps/',
|
||||
path: '/apps/',
|
||||
|
@ -63,6 +70,7 @@ export interface FileRoutesByFullPath {
|
|||
'/login': typeof AuthLoginIndexRoute
|
||||
'/agent': typeof AuthenticatedAgentIndexRoute
|
||||
'/apps': typeof AuthenticatedAppsIndexRoute
|
||||
'/command': typeof AuthenticatedCommandIndexRoute
|
||||
'/room': typeof AuthenticatedRoomIndexRoute
|
||||
'/room/$roomName': typeof AuthenticatedRoomRoomNameIndexRoute
|
||||
}
|
||||
|
@ -71,6 +79,7 @@ export interface FileRoutesByTo {
|
|||
'/login': typeof AuthLoginIndexRoute
|
||||
'/agent': typeof AuthenticatedAgentIndexRoute
|
||||
'/apps': typeof AuthenticatedAppsIndexRoute
|
||||
'/command': typeof AuthenticatedCommandIndexRoute
|
||||
'/room': typeof AuthenticatedRoomIndexRoute
|
||||
'/room/$roomName': typeof AuthenticatedRoomRoomNameIndexRoute
|
||||
}
|
||||
|
@ -82,14 +91,29 @@ export interface FileRoutesById {
|
|||
'/_auth/login/': typeof AuthLoginIndexRoute
|
||||
'/_authenticated/agent/': typeof AuthenticatedAgentIndexRoute
|
||||
'/_authenticated/apps/': typeof AuthenticatedAppsIndexRoute
|
||||
'/_authenticated/command/': typeof AuthenticatedCommandIndexRoute
|
||||
'/_authenticated/room/': typeof AuthenticatedRoomIndexRoute
|
||||
'/_authenticated/room/$roomName/': typeof AuthenticatedRoomRoomNameIndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/login' | '/agent' | '/apps' | '/room' | '/room/$roomName'
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/login'
|
||||
| '/agent'
|
||||
| '/apps'
|
||||
| '/command'
|
||||
| '/room'
|
||||
| '/room/$roomName'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/login' | '/agent' | '/apps' | '/room' | '/room/$roomName'
|
||||
to:
|
||||
| '/'
|
||||
| '/login'
|
||||
| '/agent'
|
||||
| '/apps'
|
||||
| '/command'
|
||||
| '/room'
|
||||
| '/room/$roomName'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
|
@ -98,6 +122,7 @@ export interface FileRouteTypes {
|
|||
| '/_auth/login/'
|
||||
| '/_authenticated/agent/'
|
||||
| '/_authenticated/apps/'
|
||||
| '/_authenticated/command/'
|
||||
| '/_authenticated/room/'
|
||||
| '/_authenticated/room/$roomName/'
|
||||
fileRoutesById: FileRoutesById
|
||||
|
@ -138,6 +163,13 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof AuthenticatedRoomIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/command/': {
|
||||
id: '/_authenticated/command/'
|
||||
path: '/command'
|
||||
fullPath: '/command'
|
||||
preLoaderRoute: typeof AuthenticatedCommandIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/apps/': {
|
||||
id: '/_authenticated/apps/'
|
||||
path: '/apps'
|
||||
|
@ -182,6 +214,7 @@ const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
|
|||
interface AuthenticatedRouteChildren {
|
||||
AuthenticatedAgentIndexRoute: typeof AuthenticatedAgentIndexRoute
|
||||
AuthenticatedAppsIndexRoute: typeof AuthenticatedAppsIndexRoute
|
||||
AuthenticatedCommandIndexRoute: typeof AuthenticatedCommandIndexRoute
|
||||
AuthenticatedRoomIndexRoute: typeof AuthenticatedRoomIndexRoute
|
||||
AuthenticatedRoomRoomNameIndexRoute: typeof AuthenticatedRoomRoomNameIndexRoute
|
||||
}
|
||||
|
@ -189,6 +222,7 @@ interface AuthenticatedRouteChildren {
|
|||
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
|
||||
AuthenticatedAgentIndexRoute: AuthenticatedAgentIndexRoute,
|
||||
AuthenticatedAppsIndexRoute: AuthenticatedAppsIndexRoute,
|
||||
AuthenticatedCommandIndexRoute: AuthenticatedCommandIndexRoute,
|
||||
AuthenticatedRoomIndexRoute: AuthenticatedRoomIndexRoute,
|
||||
AuthenticatedRoomRoomNameIndexRoute: AuthenticatedRoomRoomNameIndexRoute,
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useMutationData } from "@/hooks/useMutationData";
|
|||
import { BASE_URL, API_ENDPOINTS } from "@/config/api";
|
||||
import { toast } from "sonner";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
|
||||
type Version = {
|
||||
id?: string;
|
||||
|
@ -12,6 +13,7 @@ type Version = {
|
|||
fileName: string;
|
||||
folderPath: string;
|
||||
updatedAt?: string;
|
||||
requestUpdateAt?: string;
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/_authenticated/agent/")({
|
||||
|
@ -26,16 +28,25 @@ function AgentsPage() {
|
|||
url: BASE_URL + API_ENDPOINTS.APP_VERSION.GET_VERSION,
|
||||
});
|
||||
|
||||
const versionList: Version[] = Array.isArray(data) ? data : data ? [data] : [];
|
||||
const versionList: Version[] = Array.isArray(data)
|
||||
? data
|
||||
: data
|
||||
? [data]
|
||||
: [];
|
||||
|
||||
// Mutation upload
|
||||
const uploadMutation = useMutationData<FormData>({
|
||||
url: BASE_URL + API_ENDPOINTS.APP_VERSION.UPLOAD,
|
||||
method: "POST",
|
||||
invalidate: [["agent-version"]],
|
||||
onSuccess: () => toast.success("Upload thành công!"),
|
||||
onError: () => toast.error("Upload thất bại!"),
|
||||
onError: (error) => {
|
||||
console.error("Upload error:", error)
|
||||
toast.error("Upload thất bại!")
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation update
|
||||
const updateMutation = useMutationData<void>({
|
||||
url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.UPDATE_AGENT,
|
||||
method: "POST",
|
||||
|
@ -44,18 +55,9 @@ function AgentsPage() {
|
|||
});
|
||||
|
||||
const columns: ColumnDef<Version>[] = [
|
||||
{
|
||||
accessorKey: "version",
|
||||
header: "Phiên bản",
|
||||
},
|
||||
{
|
||||
accessorKey: "fileName",
|
||||
header: "Tên file",
|
||||
},
|
||||
{
|
||||
accessorKey: "folderPath",
|
||||
header: "Đường dẫn",
|
||||
},
|
||||
{ accessorKey: "version", header: "Phiên bản" },
|
||||
{ accessorKey: "fileName", header: "Tên file" },
|
||||
{ accessorKey: "folderPath", header: "Đường dẫn" },
|
||||
{
|
||||
accessorKey: "updatedAt",
|
||||
header: "Thời gian cập nhật",
|
||||
|
@ -64,8 +66,32 @@ function AgentsPage() {
|
|||
? new Date(getValue() as string).toLocaleString("vi-VN")
|
||||
: "N/A",
|
||||
},
|
||||
{
|
||||
accessorKey: "requestUpdateAt",
|
||||
header: "Thời gian yêu cầu cập nhật",
|
||||
cell: ({ getValue }) =>
|
||||
getValue()
|
||||
? new Date(getValue() as string).toLocaleString("vi-VN")
|
||||
: "N/A",
|
||||
}
|
||||
];
|
||||
|
||||
const handleUpload = async (
|
||||
fd: FormData,
|
||||
config?: { onUploadProgress?: (e: AxiosProgressEvent) => void }
|
||||
) => {
|
||||
return uploadMutation.mutateAsync({
|
||||
data: fd,
|
||||
config
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdate = async () => {
|
||||
return updateMutation.mutateAsync({
|
||||
data: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AppManagerTemplate<Version>
|
||||
title="Quản lý Agent"
|
||||
|
@ -73,8 +99,8 @@ function AgentsPage() {
|
|||
data={versionList}
|
||||
isLoading={isLoading}
|
||||
columns={columns}
|
||||
onUpload={(fd) => uploadMutation.mutateAsync(fd)}
|
||||
onUpdate={() => updateMutation.mutateAsync()}
|
||||
onUpload={handleUpload}
|
||||
onUpdate={handleUpdate}
|
||||
updateLoading={updateMutation.isPending}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -6,6 +6,7 @@ import { BASE_URL, API_ENDPOINTS } from "@/config/api";
|
|||
import { toast } from "sonner";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { useState } from "react";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
|
||||
export const Route = createFileRoute("/_authenticated/apps/")({
|
||||
head: () => ({ meta: [{ title: "Quản lý phần mềm" }] }),
|
||||
|
@ -18,6 +19,7 @@ type Version = {
|
|||
fileName: string;
|
||||
folderPath: string;
|
||||
updatedAt?: string;
|
||||
requestUpdateAt?: string;
|
||||
};
|
||||
|
||||
function AppsComponent() {
|
||||
|
@ -31,34 +33,32 @@ function AppsComponent() {
|
|||
: data
|
||||
? [data]
|
||||
: [];
|
||||
|
||||
const [table, setTable] = useState<any>();
|
||||
|
||||
const uploadMutation = useMutationData<FormData>({
|
||||
url: BASE_URL + API_ENDPOINTS.APP_VERSION.UPLOAD,
|
||||
method: "POST",
|
||||
invalidate: [["software-version"]], // Add this to refresh data after upload
|
||||
onSuccess: () => toast.success("Upload thành công!"),
|
||||
onError: () => toast.error("Upload thất bại!"),
|
||||
onError: (error) => {
|
||||
console.error("Upload error:", error);
|
||||
toast.error("Upload thất bại!");
|
||||
},
|
||||
});
|
||||
|
||||
const installMutation = useMutationData<{ msiFileIds: number[] }>({
|
||||
url: BASE_URL+ API_ENDPOINTS.DEVICE_COMM.DOWNLOAD_MSI,
|
||||
const installMutation = useMutationData<{ MsiFileIds: number[] }>({
|
||||
url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.DOWNLOAD_MSI,
|
||||
method: "POST",
|
||||
onSuccess: () => toast.success("Đã gửi yêu cầu cài đặt MSI!"),
|
||||
onError: () => toast.error("Gửi yêu cầu thất bại!"),
|
||||
onError: (error) => {
|
||||
console.error("Install error:", error);
|
||||
toast.error("Gửi yêu cầu thất bại!");
|
||||
},
|
||||
});
|
||||
|
||||
const columns: ColumnDef<Version>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: () => <span>Thêm vào danh sách yêu cầu</span>,
|
||||
cell: ({ row }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.getIsSelected?.() ?? false}
|
||||
onChange={row.getToggleSelectedHandler?.()}
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
|
||||
{ accessorKey: "version", header: "Phiên bản" },
|
||||
{ accessorKey: "fileName", header: "Tên file" },
|
||||
{ accessorKey: "folderPath", header: "Đường dẫn" },
|
||||
|
@ -70,8 +70,58 @@ function AppsComponent() {
|
|||
? new Date(getValue() as string).toLocaleString("vi-VN")
|
||||
: "N/A",
|
||||
},
|
||||
{
|
||||
accessorKey: "requestUpdateAt",
|
||||
header: "Thời gian yêu cầu cài đặt",
|
||||
},
|
||||
{
|
||||
id: "select",
|
||||
header: () => <span>Thêm vào danh sách yêu cầu</span>,
|
||||
cell: ({ row }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.getIsSelected?.() ?? false}
|
||||
onChange={row.getToggleSelectedHandler?.()}
|
||||
disabled={installMutation.isPending}
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
}
|
||||
];
|
||||
|
||||
const handleUpload = async (
|
||||
fd: FormData,
|
||||
config?: { onUploadProgress?: (e: AxiosProgressEvent) => void }
|
||||
) => {
|
||||
return uploadMutation.mutateAsync({
|
||||
data: fd,
|
||||
config,
|
||||
});
|
||||
};
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (!table) {
|
||||
toast.error("Không thể lấy thông tin bảng!");
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
|
||||
if (selectedRows.length === 0) {
|
||||
toast.error("Vui lòng chọn ít nhất một file để cài đặt!");
|
||||
return;
|
||||
}
|
||||
|
||||
const MsiFileIds = selectedRows.map((row: any) => row.original.id);
|
||||
|
||||
console.log("Selected MSI file IDs:", MsiFileIds);
|
||||
|
||||
return installMutation.mutateAsync({
|
||||
data: { MsiFileIds },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AppManagerTemplate<Version>
|
||||
title="Quản lý phần mềm"
|
||||
|
@ -79,16 +129,10 @@ function AppsComponent() {
|
|||
data={versionList}
|
||||
isLoading={isLoading}
|
||||
columns={columns}
|
||||
onUpload={(fd) => uploadMutation.mutateAsync(fd)}
|
||||
onTableInit={setTable}
|
||||
onUpdate={() => {
|
||||
const selectedIds = table
|
||||
?.getSelectedRowModel()
|
||||
.rows.map((row: any) => (row.original as Version).id);
|
||||
|
||||
installMutation.mutateAsync({ msiFileIds: selectedIds });
|
||||
}}
|
||||
onUpload={handleUpload}
|
||||
onUpdate={handleInstall}
|
||||
updateLoading={installMutation.isPending}
|
||||
onTableInit={setTable}
|
||||
/>
|
||||
);
|
||||
}
|
46
src/routes/_authenticated/command/index.tsx
Normal file
46
src/routes/_authenticated/command/index.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { FormSubmitTemplate } from "@/template/form-submit-template";
|
||||
import { ShellCommandForm } from "@/components/command-form";
|
||||
import { useMutationData } from "@/hooks/useMutationData";
|
||||
import { BASE_URL, API_ENDPOINTS } from "@/config/api";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const Route = createFileRoute("/_authenticated/command/")({
|
||||
head: () => ({ meta: [{ title: "Gửi lệnh CMD" }] }),
|
||||
component: CommandPage,
|
||||
});
|
||||
|
||||
function CommandPage() {
|
||||
const sendCommandMutation = useMutationData<
|
||||
string,
|
||||
{ success: boolean; output: string }
|
||||
>({
|
||||
url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.SEND_COMMAND,
|
||||
method: "POST",
|
||||
onSuccess: (data) => {
|
||||
if (data.success) {
|
||||
toast.success("Lệnh đã được gửi thành công!");
|
||||
} else {
|
||||
toast.error("Lệnh không thể thực thi trên thiết bị!");
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Send command error:", error);
|
||||
toast.error("Gửi lệnh thất bại!");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<FormSubmitTemplate
|
||||
title="CMD Command"
|
||||
description="Gửi lệnh shell xuống thiết bị để thực thi"
|
||||
isLoading={sendCommandMutation.isPending}
|
||||
>
|
||||
<ShellCommandForm
|
||||
onExecute={async (cmd: string) => {
|
||||
return await sendCommandMutation.mutateAsync({ data: cmd });
|
||||
}}
|
||||
/>
|
||||
</FormSubmitTemplate>
|
||||
);
|
||||
}
|
|
@ -1,16 +1,9 @@
|
|||
import {
|
||||
createFileRoute,
|
||||
Outlet,
|
||||
redirect,
|
||||
} from "@tanstack/react-router";
|
||||
import AppLayout from "@/layouts/app-layout";
|
||||
export const Route = createFileRoute("/")({
|
||||
// beforeLoad: async ({context}) => {
|
||||
// const {authToken} = context.auth
|
||||
// if (!authToken) {
|
||||
// throw redirect({to: '/login'})
|
||||
// }
|
||||
// },
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
|
|
|
@ -11,6 +11,7 @@ import { FileText } from "lucide-react";
|
|||
import { UploadDialog } from "@/components/upload-dialog";
|
||||
import { VersionTable } from "@/components/version-table";
|
||||
import { UpdateButton } from "@/components/update-button";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
|
||||
interface AppManagerTemplateProps<TData> {
|
||||
title: string;
|
||||
|
@ -18,7 +19,7 @@ interface AppManagerTemplateProps<TData> {
|
|||
data: TData[];
|
||||
isLoading: boolean;
|
||||
columns: ColumnDef<TData, any>[];
|
||||
onUpload: (fd: FormData) => Promise<void>;
|
||||
onUpload: (fd: FormData, config?: { onUploadProgress?: (e: AxiosProgressEvent) => void }) => Promise<void>;
|
||||
onUpdate?: () => void;
|
||||
updateLoading?: boolean;
|
||||
onTableInit?: (table: any) => void;
|
||||
|
|
53
src/template/form-submit-template.tsx
Normal file
53
src/template/form-submit-template.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { ShellCommandForm } from "@/components/command-form";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { UpdateButton } from "@/components/update-button";
|
||||
import { FileText, Terminal } from "lucide-react";
|
||||
|
||||
interface FormSubmitTemplateProps {
|
||||
title: string;
|
||||
description: string;
|
||||
isLoading?: boolean;
|
||||
children: React.ReactNode;
|
||||
onSubmit?: () => void;
|
||||
submitLoading?: boolean;
|
||||
}
|
||||
|
||||
export function FormSubmitTemplate({
|
||||
title,
|
||||
description,
|
||||
isLoading,
|
||||
children,
|
||||
onSubmit,
|
||||
submitLoading,
|
||||
}: FormSubmitTemplateProps) {
|
||||
return (
|
||||
<div className="w-full px-6 space-y-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">{title}</h1>
|
||||
<p className="text-muted-foreground mt-2">{description}</p>
|
||||
</div>
|
||||
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Terminal className="h-5 w-5" /> Gửi lệnh CMD
|
||||
</CardTitle>
|
||||
<CardDescription>Nhập và gửi lệnh xuống thiết bị</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>{children}</CardContent>
|
||||
{onSubmit && (
|
||||
<CardFooter>
|
||||
<UpdateButton onClick={onSubmit} loading={submitLoading} />
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user