mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2025-11-04 18:32:51 +00:00
Compare commits
11 Commits
2025-01-30
...
2025-01-31
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9b84bf5ee | ||
|
|
ccab9d1be5 | ||
|
|
58a2ece7b7 | ||
|
|
aa16f936c8 | ||
|
|
c8829beddd | ||
|
|
3adc22d837 | ||
|
|
71b1288220 | ||
|
|
3c58303a9f | ||
|
|
b8edf0dd68 | ||
|
|
2fa3116c9c | ||
|
|
d416ff9cfa |
18
CHANGELOG.md
18
CHANGELOG.md
@@ -17,6 +17,24 @@ All LXC instances created using this repository come pre-installed with Midnight
|
|||||||
Do not break established syntax in this file, as it is automatically updated by a Github Workflow
|
Do not break established syntax in this file, as it is automatically updated by a Github Workflow
|
||||||
|
|
||||||
|
|
||||||
|
## 2025-01-31
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### ✨ New Scripts
|
||||||
|
|
||||||
|
- New Script: Paymenter [@opastorello](https://github.com/opastorello) ([#1827](https://github.com/community-scripts/ProxmoxVE/pull/1827))
|
||||||
|
|
||||||
|
### 🚀 Updated Scripts
|
||||||
|
|
||||||
|
- [Fix] Alpine-IT-Tools, add missing ssh package for root ssh access [@CrazyWolf13](https://github.com/CrazyWolf13) ([#1891](https://github.com/community-scripts/ProxmoxVE/pull/1891))
|
||||||
|
- [Fix] Change Download of Trilium after there change the tag/release logic [@MickLesk](https://github.com/MickLesk) ([#1892](https://github.com/community-scripts/ProxmoxVE/pull/1892))
|
||||||
|
|
||||||
|
### 🌐 Website
|
||||||
|
|
||||||
|
- [Website] Enhance DataFetcher with better UI components and add reactive data fetching intervals [@BramSuurdje](https://github.com/BramSuurdje) ([#1902](https://github.com/community-scripts/ProxmoxVE/pull/1902))
|
||||||
|
- [Website] Update /data/page.tsx [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#1900](https://github.com/community-scripts/ProxmoxVE/pull/1900))
|
||||||
|
|
||||||
## 2025-01-30
|
## 2025-01-30
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
6
ct/headers/paymenter
Normal file
6
ct/headers/paymenter
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
____ __
|
||||||
|
/ __ \____ ___ ______ ___ ___ ____ / /____ _____
|
||||||
|
/ /_/ / __ `/ / / / __ `__ \/ _ \/ __ \/ __/ _ \/ ___/
|
||||||
|
/ ____/ /_/ / /_/ / / / / / / __/ / / / /_/ __/ /
|
||||||
|
/_/ \__,_/\__, /_/ /_/ /_/\___/_/ /_/\__/\___/_/
|
||||||
|
/____/
|
||||||
56
ct/paymenter.sh
Normal file
56
ct/paymenter.sh
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
|
||||||
|
|
||||||
|
# Copyright (c) 2021-2025 community-scripts ORG
|
||||||
|
# Author: Nícolas Pastorello (opastorello)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
|
||||||
|
# App Default Values
|
||||||
|
APP="Paymenter"
|
||||||
|
var_tags="hosting;ecommerce;marketplace;"
|
||||||
|
var_cpu="2"
|
||||||
|
var_ram="1024"
|
||||||
|
var_disk="5"
|
||||||
|
var_os="Debian"
|
||||||
|
var_version="12"
|
||||||
|
var_unprivileged="1"
|
||||||
|
|
||||||
|
# App Output & Base Settings
|
||||||
|
header_info "$APP"
|
||||||
|
base_settings
|
||||||
|
|
||||||
|
# Core
|
||||||
|
variables
|
||||||
|
color
|
||||||
|
catch_errors
|
||||||
|
|
||||||
|
function update_script() {
|
||||||
|
header_info
|
||||||
|
check_container_storage
|
||||||
|
check_container_resources
|
||||||
|
|
||||||
|
if [[ ! -d /opt/paymenter ]]; then
|
||||||
|
msg_error "No ${APP} Installation Found!"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
RELEASE=$(curl -s https://api.github.com/repos/paymenter/paymenter/releases/latest | grep '"tag_name"' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/')
|
||||||
|
if [[ ! -f /opt/${APP}_version.txt ]] || [[ "${RELEASE}" != "$(cat /opt/${APP}_version.txt)" ]]; then
|
||||||
|
msg_info "Updating ${APP} to ${RELEASE}"
|
||||||
|
echo "${RELEASE}" >/opt/${APP}_version.txt
|
||||||
|
cd /opt/paymenter
|
||||||
|
php artisan p:upgrade --no-interaction &>/dev/null
|
||||||
|
msg_ok "Updated Successfully"
|
||||||
|
else
|
||||||
|
msg_ok "No update required. ${APP} is already at ${RELEASE}."
|
||||||
|
fi
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
start
|
||||||
|
build_container
|
||||||
|
description
|
||||||
|
|
||||||
|
msg_ok "Completed Successfully!\n"
|
||||||
|
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||||
|
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
|
||||||
|
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:80${CL}"
|
||||||
@@ -35,22 +35,22 @@ function update_script() {
|
|||||||
RELEASE=$(curl -s https://api.github.com/repos/TriliumNext/Notes/releases/latest | grep "tag_name" | awk '{print substr($2, 2, length($2)-3) }')
|
RELEASE=$(curl -s https://api.github.com/repos/TriliumNext/Notes/releases/latest | grep "tag_name" | awk '{print substr($2, 2, length($2)-3) }')
|
||||||
|
|
||||||
msg_info "Stopping ${APP}"
|
msg_info "Stopping ${APP}"
|
||||||
systemctl stop trilium.service
|
systemctl stop trilium
|
||||||
sleep 1
|
sleep 1
|
||||||
msg_ok "Stopped ${APP}"
|
msg_ok "Stopped ${APP}"
|
||||||
|
|
||||||
msg_info "Updating to ${RELEASE}"
|
msg_info "Updating to ${RELEASE}"
|
||||||
wget -q https://github.com/TriliumNext/Notes/releases/download/${RELEASE}/TriliumNextNotes-${RELEASE}-server-linux-x64.tar.xz
|
wget -q https://github.com/TriliumNext/Notes/releases/download/${RELEASE}/TriliumNextNotes-linux-x64-${RELEASE}.tar.xz
|
||||||
tar -xf TriliumNextNotes-${RELEASE}-server-linux-x64.tar.xz
|
tar -xf TriliumNextNotes-linux-x64-${RELEASE}.tar.xz
|
||||||
cp -r trilium-linux-x64-server/* /opt/trilium/
|
cp -r trilium-linux-x64-server/* /opt/trilium/
|
||||||
msg_ok "Updated to ${RELEASE}"
|
msg_ok "Updated to ${RELEASE}"
|
||||||
|
|
||||||
msg_info "Cleaning up"
|
msg_info "Cleaning up"
|
||||||
rm -rf TriliumNextNotes-${RELEASE}-server-linux-x64.tar.xz trilium-linux-x64-server
|
rm -rf TriliumNextNotes-linux-x64-${RELEASE}.tar.xz trilium-linux-x64-server
|
||||||
msg_ok "Cleaned"
|
msg_ok "Cleaned"
|
||||||
|
|
||||||
msg_info "Starting ${APP}"
|
msg_info "Starting ${APP}"
|
||||||
systemctl start trilium.service
|
systemctl start trilium
|
||||||
sleep 1
|
sleep 1
|
||||||
msg_ok "Started ${APP}"
|
msg_ok "Started ${APP}"
|
||||||
msg_ok "Updated Successfully"
|
msg_ok "Updated Successfully"
|
||||||
|
|||||||
@@ -1,10 +1,33 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import ApplicationChart from "@/components/ApplicationChart";
|
||||||
import DatePicker from 'react-datepicker';
|
import { Button } from "@/components/ui/button";
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
import { string } from "zod";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import ApplicationChart from "../../components/ApplicationChart";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { Calendar as CalendarIcon } from "lucide-react";
|
||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
interface DataModel {
|
interface DataModel {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -35,27 +58,47 @@ const DataFetcher: React.FC = () => {
|
|||||||
const [startDate, setStartDate] = useState<Date | null>(null);
|
const [startDate, setStartDate] = useState<Date | null>(null);
|
||||||
const [endDate, setEndDate] = useState<Date | null>(null);
|
const [endDate, setEndDate] = useState<Date | null>(null);
|
||||||
const [sortConfig, setSortConfig] = useState<{ key: keyof DataModel | null, direction: 'ascending' | 'descending' }>({ key: 'id', direction: 'descending' });
|
const [sortConfig, setSortConfig] = useState<{ key: keyof DataModel | null, direction: 'ascending' | 'descending' }>({ key: 'id', direction: 'descending' });
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(5);
|
const [itemsPerPage, setItemsPerPage] = useState(25);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [showChart, setShowChart] = useState<boolean>(false);
|
const [interval, setIntervalTime] = useState<number>(10); // Default interval 10 seconds
|
||||||
|
const [reloadInterval, setReloadInterval] = useState<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
const fetchData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("https://api.htl-braunau.at/data/json");
|
const response = await fetch("https://api.htl-braunau.at/data/json");
|
||||||
if (!response.ok) throw new Error("Failed to fetch data: ${response.statusText}");
|
if (!response.ok) throw new Error(`Failed to fetch data: ${response.statusText}`);
|
||||||
const result: DataModel[] = await response.json();
|
const result: DataModel[] = await response.json();
|
||||||
setData(result);
|
setData(result);
|
||||||
|
setLoading(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError((err as Error).message);
|
setError((err as Error).message);
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
const storedInterval = localStorage.getItem('reloadInterval');
|
||||||
|
if (storedInterval) {
|
||||||
|
setIntervalTime(Number(storedInterval));
|
||||||
|
}
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let intervalId: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
if (interval > 0) {
|
||||||
|
intervalId = setInterval(fetchData, Math.max(interval, 10) * 1000);
|
||||||
|
localStorage.setItem('reloadInterval', interval.toString());
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('reloadInterval');
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (intervalId) clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, [interval, fetchData]);
|
||||||
|
|
||||||
const filteredData = data.filter(item => {
|
const filteredData = data.filter(item => {
|
||||||
const matchesSearchQuery = Object.values(item).some(value =>
|
const matchesSearchQuery = Object.values(item).some(value =>
|
||||||
@@ -110,93 +153,146 @@ const DataFetcher: React.FC = () => {
|
|||||||
return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`;
|
return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleItemsPerPageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
|
||||||
setItemsPerPage(Number(event.target.value));
|
|
||||||
setCurrentPage(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const paginatedData = sortedData.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
|
const paginatedData = sortedData.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
|
||||||
|
|
||||||
if (loading) return <p>Loading...</p>;
|
const statusCounts = data.reduce((acc, item) => {
|
||||||
if (error) return <p>Error: {error}</p>;
|
const status = item.status;
|
||||||
|
acc[status] = (acc[status] || 0) + 1;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, number>);
|
||||||
|
|
||||||
|
if (loading) return <div className="flex justify-center items-center h-screen">Loading...</div>;
|
||||||
|
if (error) return <div className="flex justify-center items-center h-screen text-red-500">Error: {error}</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 mt-20">
|
<div className="container mx-auto p-6 pt-20 space-y-6">
|
||||||
<h1 className="text-2xl font-bold mb-4 text-center">Created LXCs</h1>
|
<h1 className="text-3xl font-bold text-center">Created LXCs</h1>
|
||||||
<div className="mb-4 flex space-x-4">
|
|
||||||
<div>
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<input
|
<Card>
|
||||||
type="text"
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Search</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Input
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={e => setSearchQuery(e.target.value)}
|
onChange={e => setSearchQuery(e.target.value)}
|
||||||
className="p-2 border"
|
|
||||||
/>
|
/>
|
||||||
<label className="text-sm text-gray-600 mt-1 block">Search by keyword</label>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
<div>
|
|
||||||
<DatePicker
|
<Card>
|
||||||
selected={startDate}
|
<CardHeader className="pb-2">
|
||||||
onChange={date => setStartDate(date)}
|
<CardTitle className="text-sm font-medium">Start Date</CardTitle>
|
||||||
selectsStart
|
</CardHeader>
|
||||||
startDate={startDate}
|
<CardContent>
|
||||||
endDate={endDate}
|
<Popover>
|
||||||
placeholderText="Start date"
|
<PopoverTrigger asChild>
|
||||||
className="p-2 border"
|
<Button variant="outline" className="w-full justify-start text-left font-normal">
|
||||||
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
|
{startDate ? format(startDate, "PPP") : "Pick a date"}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={startDate || undefined}
|
||||||
|
onSelect={(date: Date | undefined) => setStartDate(date || null)}
|
||||||
|
initialFocus
|
||||||
/>
|
/>
|
||||||
<label className="text-sm text-gray-600 mt-1 block">Set a start date</label>
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">End Date</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline" className="w-full justify-start text-left font-normal">
|
||||||
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
|
{endDate ? format(endDate, "PPP") : "Pick a date"}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={endDate || undefined}
|
||||||
|
onSelect={(date: Date | undefined) => setEndDate(date || null)}
|
||||||
|
initialFocus
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Reload Interval</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={interval}
|
||||||
|
onChange={e => setIntervalTime(Number(e.target.value))}
|
||||||
|
placeholder="Interval (seconds)"
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<DatePicker
|
|
||||||
selected={endDate}
|
|
||||||
onChange={date => setEndDate(date)}
|
|
||||||
selectsEnd
|
|
||||||
startDate={startDate}
|
|
||||||
endDate={endDate}
|
|
||||||
placeholderText="End date"
|
|
||||||
className="p-2 border"
|
|
||||||
/>
|
|
||||||
<label className="text-sm text-gray-600 mt-1 block">Set a end date</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ApplicationChart data={filteredData} />
|
<ApplicationChart data={filteredData} />
|
||||||
<div className="mb-4 flex justify-between items-center">
|
|
||||||
<p className="text-lg font-bold">{filteredData.length} results found</p>
|
<div className="flex justify-between items-center">
|
||||||
<select value={itemsPerPage} onChange={handleItemsPerPageChange} className="p-2 border">
|
<p className="text-lg font-medium">{filteredData.length} results found</p>
|
||||||
<option value={25}>25</option>
|
<div className="flex gap-2 items-center">
|
||||||
<option value={50}>50</option>
|
<span>🔄 Installing: {statusCounts.installing || 0}</span>
|
||||||
<option value={100}>100</option>
|
<span>✔️ Completed: {statusCounts.done || 0}</span>
|
||||||
<option value={200}>200</option>
|
<span>❌ Failed: {statusCounts.failed || 0}</span>
|
||||||
</select>
|
<span>❓ Unknown: {statusCounts.unknown || 0}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto">
|
<Select value={itemsPerPage.toString()} onValueChange={(value) => setItemsPerPage(Number(value))}>
|
||||||
<div className="overflow-y-auto lg:overflow-y-visible">
|
<SelectTrigger className="w-[180px]">
|
||||||
<table className="min-w-full table-auto border-collapse">
|
<SelectValue placeholder="Items per page" />
|
||||||
<thead>
|
</SelectTrigger>
|
||||||
<tr>
|
<SelectContent>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('status')}>Status</th>
|
{[25, 50, 100, 200].map(value => (
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('nsapp')}>Application</th>
|
<SelectItem key={value} value={value.toString()}>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_type')}>OS</th>
|
{value} items
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_version')}>OS Version</th>
|
</SelectItem>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('disk_size')}>Disk Size</th>
|
))}
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('core_count')}>Core Count</th>
|
</SelectContent>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('ram_size')}>RAM Size</th>
|
</Select>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('hn')}>Hostname</th>
|
</div>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('ssh')}>SSH</th>
|
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('verbose')}>Verb</th>
|
<div className="rounded-md border">
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('tags')}>Tags</th>
|
<Table>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('method')}>Method</th>
|
<TableHeader>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('pve_version')}>PVE Version</th>
|
<TableRow>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('created_at')}>Created At</th>
|
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('status')}>Status</TableHead>
|
||||||
</tr>
|
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('nsapp')}>Application</TableHead>
|
||||||
</thead>
|
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_type')}>OS</TableHead>
|
||||||
<tbody>
|
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_version')}>OS Version</TableHead>
|
||||||
|
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('disk_size')}>Disk Size</TableHead>
|
||||||
|
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('core_count')}>Core Count</TableHead>
|
||||||
|
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('ram_size')}>RAM Size</TableHead>
|
||||||
|
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('hn')}>Hostname</TableHead>
|
||||||
|
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('ssh')}>SSH</TableHead>
|
||||||
|
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('verbose')}>Verb</TableHead>
|
||||||
|
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('tags')}>Tags</TableHead>
|
||||||
|
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('method')}>Method</TableHead>
|
||||||
|
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('pve_version')}>PVE Version</TableHead>
|
||||||
|
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('created_at')}>Created At</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
{paginatedData.map((item, index) => (
|
{paginatedData.map((item, index) => (
|
||||||
<tr key={index}>
|
<TableRow key={index}>
|
||||||
<td className="px-4 py-2 border-b">
|
<TableCell className="px-4 py-2 border-b">{item.status === "done" ? (
|
||||||
{item.status === "done" ? (
|
|
||||||
"✔️"
|
"✔️"
|
||||||
) : item.status === "failed" ? (
|
) : item.status === "failed" ? (
|
||||||
"❌"
|
"❌"
|
||||||
@@ -204,45 +300,47 @@ const DataFetcher: React.FC = () => {
|
|||||||
"🔄"
|
"🔄"
|
||||||
) : (
|
) : (
|
||||||
item.status
|
item.status
|
||||||
)}
|
)}</TableCell>
|
||||||
</td>
|
<TableCell className="px-4 py-2 border-b">{item.nsapp}</TableCell>
|
||||||
<td className="px-4 py-2 border-b">{item.nsapp}</td>
|
<TableCell className="px-4 py-2 border-b">{item.os_type}</TableCell>
|
||||||
<td className="px-4 py-2 border-b">{item.os_type}</td>
|
<TableCell className="px-4 py-2 border-b">{item.os_version}</TableCell>
|
||||||
<td className="px-4 py-2 border-b">{item.os_version}</td>
|
<TableCell className="px-4 py-2 border-b">{item.disk_size}</TableCell>
|
||||||
<td className="px-4 py-2 border-b">{item.disk_size}</td>
|
<TableCell className="px-4 py-2 border-b">{item.core_count}</TableCell>
|
||||||
<td className="px-4 py-2 border-b">{item.core_count}</td>
|
<TableCell className="px-4 py-2 border-b">{item.ram_size}</TableCell>
|
||||||
<td className="px-4 py-2 border-b">{item.ram_size}</td>
|
<TableCell className="px-4 py-2 border-b">{item.hn}</TableCell>
|
||||||
<td className="px-4 py-2 border-b">{item.hn}</td>
|
<TableCell className="px-4 py-2 border-b">{item.ssh}</TableCell>
|
||||||
<td className="px-4 py-2 border-b">{item.ssh}</td>
|
<TableCell className="px-4 py-2 border-b">{item.verbose}</TableCell>
|
||||||
<td className="px-4 py-2 border-b">{item.verbose}</td>
|
<TableCell className="px-4 py-2 border-b">{item.tags.replace(/;/g, ' ')}</TableCell>
|
||||||
<td className="px-4 py-2 border-b">{item.tags.replace(/;/g, ' ')}</td>
|
<TableCell className="px-4 py-2 border-b">{item.method}</TableCell>
|
||||||
<td className="px-4 py-2 border-b">{item.method}</td>
|
<TableCell className="px-4 py-2 border-b">{item.pve_version}</TableCell>
|
||||||
<td className="px-4 py-2 border-b">{item.pve_version}</td>
|
<TableCell className="px-4 py-2 border-b">{formatDate(item.created_at)}</TableCell>
|
||||||
<td className="px-4 py-2 border-b">{formatDate(item.created_at)}</td>
|
</TableRow>
|
||||||
</tr>
|
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</TableBody>
|
||||||
</table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="mt-4 flex justify-between items-center">
|
<div className="flex items-center justify-center space-x-2">
|
||||||
<button
|
<Button
|
||||||
|
variant="outline"
|
||||||
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
|
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
className="p-2 border"
|
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</Button>
|
||||||
<span>Page {currentPage}</span>
|
<span className="text-sm">
|
||||||
<button
|
Page {currentPage} of {Math.ceil(sortedData.length / itemsPerPage)}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
onClick={() => setCurrentPage(prev => (prev * itemsPerPage < sortedData.length ? prev + 1 : prev))}
|
onClick={() => setCurrentPage(prev => (prev * itemsPerPage < sortedData.length ? prev + 1 : prev))}
|
||||||
disabled={currentPage * itemsPerPage >= sortedData.length}
|
disabled={currentPage * itemsPerPage >= sortedData.length}
|
||||||
className="p-2 border"
|
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DataFetcher;
|
export default DataFetcher;
|
||||||
|
|||||||
@@ -1,132 +1,193 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { Chart as ChartJS, ArcElement, Tooltip as ChartTooltip, Legend } from "chart.js";
|
||||||
|
import ChartDataLabels from "chartjs-plugin-datalabels";
|
||||||
|
import { BarChart3, PieChart } from "lucide-react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Pie } from "react-chartjs-2";
|
import { Pie } from "react-chartjs-2";
|
||||||
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
|
|
||||||
import ChartDataLabels from "chartjs-plugin-datalabels";
|
|
||||||
import Modal from "@/components/Modal";
|
|
||||||
|
|
||||||
ChartJS.register(ArcElement, Tooltip, Legend, ChartDataLabels);
|
ChartJS.register(ArcElement, ChartTooltip, Legend, ChartDataLabels);
|
||||||
|
|
||||||
interface ApplicationChartProps {
|
interface ApplicationChartProps {
|
||||||
data: { nsapp: string }[];
|
data: { nsapp: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const ApplicationChart: React.FC<ApplicationChartProps> = ({ data }) => {
|
const ITEMS_PER_PAGE = 20;
|
||||||
const [isChartOpen, setIsChartOpen] = useState(false);
|
const CHART_COLORS = [
|
||||||
const [isTableOpen, setIsTableOpen] = useState(false);
|
|
||||||
const [chartStartIndex, setChartStartIndex] = useState(0);
|
|
||||||
const [tableLimit, setTableLimit] = useState(20);
|
|
||||||
|
|
||||||
const appCounts: Record<string, number> = {};
|
|
||||||
data.forEach((item) => {
|
|
||||||
appCounts[item.nsapp] = (appCounts[item.nsapp] || 0) + 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
const sortedApps = Object.entries(appCounts).sort(([, a], [, b]) => b - a);
|
|
||||||
const chartApps = sortedApps.slice(chartStartIndex, chartStartIndex + 20);
|
|
||||||
|
|
||||||
const chartData = {
|
|
||||||
labels: chartApps.map(([name]) => name),
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "Applications",
|
|
||||||
data: chartApps.map(([, count]) => count),
|
|
||||||
backgroundColor: [
|
|
||||||
"#ff6384",
|
"#ff6384",
|
||||||
"#36a2eb",
|
"#36a2eb",
|
||||||
"#ffce56",
|
"#ffce56",
|
||||||
"#4bc0c0",
|
"#4bc0c0",
|
||||||
"#9966ff",
|
"#9966ff",
|
||||||
"#ff9f40",
|
"#ff9f40",
|
||||||
],
|
"#4dc9f6",
|
||||||
|
"#f67019",
|
||||||
|
"#537bc4",
|
||||||
|
"#acc236",
|
||||||
|
"#166a8f",
|
||||||
|
"#00a950",
|
||||||
|
"#58595b",
|
||||||
|
"#8549ba",
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ApplicationChart({ data }: ApplicationChartProps) {
|
||||||
|
const [isChartOpen, setIsChartOpen] = useState(false);
|
||||||
|
const [isTableOpen, setIsTableOpen] = useState(false);
|
||||||
|
const [chartStartIndex, setChartStartIndex] = useState(0);
|
||||||
|
const [tableLimit, setTableLimit] = useState(ITEMS_PER_PAGE);
|
||||||
|
|
||||||
|
// Calculate application counts
|
||||||
|
const appCounts = data.reduce((acc, item) => {
|
||||||
|
acc[item.nsapp] = (acc[item.nsapp] || 0) + 1;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, number>);
|
||||||
|
|
||||||
|
const sortedApps = Object.entries(appCounts)
|
||||||
|
.sort(([, a], [, b]) => b - a);
|
||||||
|
|
||||||
|
const chartApps = sortedApps.slice(
|
||||||
|
chartStartIndex,
|
||||||
|
chartStartIndex + ITEMS_PER_PAGE
|
||||||
|
);
|
||||||
|
|
||||||
|
const chartData = {
|
||||||
|
labels: chartApps.map(([name]) => name),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: chartApps.map(([, count]) => count),
|
||||||
|
backgroundColor: CHART_COLORS,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const chartOptions = {
|
||||||
<div className="mt-6 text-center">
|
|
||||||
<button
|
|
||||||
onClick={() => setIsChartOpen(true)}
|
|
||||||
className="m-2 p-2 bg-blue-500 text-white rounded"
|
|
||||||
>
|
|
||||||
📊 Open Chart
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsTableOpen(true)}
|
|
||||||
className="m-2 p-2 bg-green-500 text-white rounded"
|
|
||||||
>
|
|
||||||
📋 Open Table
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<Modal isOpen={isChartOpen} onClose={() => setIsChartOpen(false)}>
|
|
||||||
<h2 className="text-xl font-bold text-black dark:text-white mb-4">Top Applications (Chart)</h2>
|
|
||||||
<div className="w-3/4 mx-auto">
|
|
||||||
<Pie
|
|
||||||
data={chartData}
|
|
||||||
options={{
|
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: { display: false },
|
legend: { display: false },
|
||||||
datalabels: {
|
datalabels: {
|
||||||
color: "white",
|
color: "white",
|
||||||
font: { weight: "bold" },
|
font: { weight: "bold" as const },
|
||||||
formatter: (value, context) =>
|
formatter: (value: number, context: any) => {
|
||||||
context.chart.data.labels?.[context.dataIndex] || "",
|
const label = context.chart.data.labels?.[context.dataIndex];
|
||||||
|
return `${label}\n(${value})`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
},
|
||||||
/>
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6 flex justify-center gap-4">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setIsChartOpen(true)}
|
||||||
|
>
|
||||||
|
<PieChart className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Open Chart View</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setIsTableOpen(true)}
|
||||||
|
>
|
||||||
|
<BarChart3 className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Open Table View</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<Dialog open={isChartOpen} onOpenChange={setIsChartOpen}>
|
||||||
|
<DialogContent className="max-w-3xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Applications Distribution</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="h-[60vh] w-full">
|
||||||
|
<Pie data={chartData} options={chartOptions} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center space-x-4 mt-4">
|
<div className="flex justify-center gap-4">
|
||||||
<button
|
<Button
|
||||||
onClick={() => setChartStartIndex(Math.max(0, chartStartIndex - 20))}
|
variant="outline"
|
||||||
|
onClick={() => setChartStartIndex(Math.max(0, chartStartIndex - ITEMS_PER_PAGE))}
|
||||||
disabled={chartStartIndex === 0}
|
disabled={chartStartIndex === 0}
|
||||||
className="p-2 border rounded bg-blue-500 text-white"
|
|
||||||
>
|
>
|
||||||
◀ Last 20
|
Previous {ITEMS_PER_PAGE}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={() => setChartStartIndex(chartStartIndex + 20)}
|
variant="outline"
|
||||||
disabled={chartStartIndex + 20 >= sortedApps.length}
|
onClick={() => setChartStartIndex(chartStartIndex + ITEMS_PER_PAGE)}
|
||||||
className="p-2 border rounded bg-blue-500 text-white"
|
disabled={chartStartIndex + ITEMS_PER_PAGE >= sortedApps.length}
|
||||||
>
|
>
|
||||||
Next 20 ▶
|
Next {ITEMS_PER_PAGE}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<Modal isOpen={isTableOpen} onClose={() => setIsTableOpen(false)}>
|
<Dialog open={isTableOpen} onOpenChange={setIsTableOpen}>
|
||||||
<h2 className="text-xl font-bold text-black dark:text-white mb-4">Application Count Table</h2>
|
<DialogContent className="max-w-2xl">
|
||||||
<table className="w-full border-collapse border border-gray-600 dark:border-gray-500">
|
<DialogHeader>
|
||||||
<thead>
|
<DialogTitle>Applications Count</DialogTitle>
|
||||||
<tr className="bg-gray-800 text-white">
|
</DialogHeader>
|
||||||
<th className="p-2 border">Application</th>
|
<div className="max-h-[60vh] overflow-y-auto">
|
||||||
<th className="p-2 border">Count</th>
|
<Table>
|
||||||
</tr>
|
<TableHeader>
|
||||||
</thead>
|
<TableRow>
|
||||||
<tbody>
|
<TableHead>Application</TableHead>
|
||||||
|
<TableHead className="text-right">Count</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
{sortedApps.slice(0, tableLimit).map(([name, count]) => (
|
{sortedApps.slice(0, tableLimit).map(([name, count]) => (
|
||||||
<tr key={name} className="hover:bg-gray-200 dark:hover:bg-gray-700 text-black dark:text-white">
|
<TableRow key={name}>
|
||||||
<td className="p-2 border">{name}</td>
|
<TableCell>{name}</TableCell>
|
||||||
<td className="p-2 border">{count}</td>
|
<TableCell className="text-right">{count}</TableCell>
|
||||||
</tr>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</TableBody>
|
||||||
</table>
|
</Table>
|
||||||
|
</div>
|
||||||
{tableLimit < sortedApps.length && (
|
{tableLimit < sortedApps.length && (
|
||||||
<div className="text-center mt-4">
|
<Button
|
||||||
<button
|
variant="outline"
|
||||||
onClick={() => setTableLimit(tableLimit + 20)}
|
className="w-full"
|
||||||
className="p-2 bg-green-500 text-white rounded"
|
onClick={() => setTableLimit(prev => prev + ITEMS_PER_PAGE)}
|
||||||
>
|
>
|
||||||
Load More
|
Load More
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default ApplicationChart;
|
|
||||||
120
frontend/src/components/ui/table.tsx
Normal file
120
frontend/src/components/ui/table.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Table = React.forwardRef<
|
||||||
|
HTMLTableElement,
|
||||||
|
React.HTMLAttributes<HTMLTableElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div className="relative w-full overflow-auto">
|
||||||
|
<table
|
||||||
|
ref={ref}
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
Table.displayName = "Table"
|
||||||
|
|
||||||
|
const TableHeader = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||||
|
))
|
||||||
|
TableHeader.displayName = "TableHeader"
|
||||||
|
|
||||||
|
const TableBody = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tbody
|
||||||
|
ref={ref}
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableBody.displayName = "TableBody"
|
||||||
|
|
||||||
|
const TableFooter = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tfoot
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableFooter.displayName = "TableFooter"
|
||||||
|
|
||||||
|
const TableRow = React.forwardRef<
|
||||||
|
HTMLTableRowElement,
|
||||||
|
React.HTMLAttributes<HTMLTableRowElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tr
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableRow.displayName = "TableRow"
|
||||||
|
|
||||||
|
const TableHead = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<th
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableHead.displayName = "TableHead"
|
||||||
|
|
||||||
|
const TableCell = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<td
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableCell.displayName = "TableCell"
|
||||||
|
|
||||||
|
const TableCaption = React.forwardRef<
|
||||||
|
HTMLTableCaptionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<caption
|
||||||
|
ref={ref}
|
||||||
|
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableCaption.displayName = "TableCaption"
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ msg_info "Installing Dependencies"
|
|||||||
$STD apk add \
|
$STD apk add \
|
||||||
curl \
|
curl \
|
||||||
mc \
|
mc \
|
||||||
|
openssh \
|
||||||
nginx \
|
nginx \
|
||||||
unzip
|
unzip
|
||||||
msg_ok "Installed Dependencies"
|
msg_ok "Installed Dependencies"
|
||||||
|
|||||||
140
install/paymenter-install.sh
Normal file
140
install/paymenter-install.sh
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Copyright (c) 2021-2025 community-scripts ORG
|
||||||
|
# Author: Nícolas Pastorello (opastorello)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
|
||||||
|
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||||
|
color
|
||||||
|
verb_ip6
|
||||||
|
catch_errors
|
||||||
|
setting_up_container
|
||||||
|
network_check
|
||||||
|
update_os
|
||||||
|
|
||||||
|
msg_info "Installing Dependencies"
|
||||||
|
$STD apt-get install -y \
|
||||||
|
curl \
|
||||||
|
sudo \
|
||||||
|
mc \
|
||||||
|
git \
|
||||||
|
software-properties-common \
|
||||||
|
apt-transport-https \
|
||||||
|
ca-certificates \
|
||||||
|
gnupg \
|
||||||
|
php8.2 \
|
||||||
|
php8.2-{common,cli,gd,mysql,mbstring,bcmath,xml,fpm,curl,zip} \
|
||||||
|
mariadb-server \
|
||||||
|
nginx \
|
||||||
|
redis-server
|
||||||
|
$STD curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||||
|
msg_ok "Installed Dependencies"
|
||||||
|
|
||||||
|
msg_info "Installing Paymenter"
|
||||||
|
RELEASE=$(curl -s https://api.github.com/repos/paymenter/paymenter/releases/latest | grep '"tag_name"' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/')
|
||||||
|
echo "${RELEASE}" >/opt/${APP}_version.txt
|
||||||
|
mkdir -p /opt/paymenter
|
||||||
|
cd /opt/paymenter
|
||||||
|
wget -q "https://github.com/paymenter/paymenter/releases/download/${RELEASE}/paymenter.tar.gz"
|
||||||
|
$STD tar -xzvf paymenter.tar.gz
|
||||||
|
chmod -R 755 storage/* bootstrap/cache/
|
||||||
|
msg_ok "Installed Paymenter"
|
||||||
|
|
||||||
|
msg_info "Setting up database"
|
||||||
|
DB_NAME=paymenter
|
||||||
|
DB_USER=paymenter
|
||||||
|
DB_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13)
|
||||||
|
mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql mysql
|
||||||
|
mysql -u root -e "CREATE DATABASE $DB_NAME;"
|
||||||
|
mysql -u root -e "CREATE USER '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASS';"
|
||||||
|
mysql -u root -e "GRANT ALL PRIVILEGES ON $DB_NAME.* TO '$DB_USER'@'localhost' WITH GRANT OPTION;"
|
||||||
|
{
|
||||||
|
echo "Paymenter Database Credentials"
|
||||||
|
echo "Database: $DB_NAME"
|
||||||
|
echo "Username: $DB_USER"
|
||||||
|
echo "Password: $DB_PASS"
|
||||||
|
} >> ~/paymenter_db.creds
|
||||||
|
cp .env.example .env
|
||||||
|
$STD composer install --no-dev --optimize-autoloader --no-interaction
|
||||||
|
$STD php artisan key:generate --force
|
||||||
|
$STD php artisan storage:link
|
||||||
|
sed -i "s/^DB_DATABASE=.*/DB_DATABASE=${DB_NAME}/" .env
|
||||||
|
sed -i "s/^DB_USERNAME=.*/DB_USERNAME=${DB_USER}/" .env
|
||||||
|
sed -i "s/^DB_PASSWORD=.*/DB_PASSWORD=${DB_PASS}/" .env
|
||||||
|
$STD php artisan migrate --force --seed
|
||||||
|
msg_ok "Set up database"
|
||||||
|
|
||||||
|
msg_info "Creating Admin User"
|
||||||
|
$STD php artisan p:user:create <<EOF
|
||||||
|
admin@paymenter.org
|
||||||
|
paymenter
|
||||||
|
admin
|
||||||
|
paymenter
|
||||||
|
0
|
||||||
|
EOF
|
||||||
|
msg_ok "Created Admin User"
|
||||||
|
|
||||||
|
msg_info "Configuring Nginx"
|
||||||
|
cat <<EOF >/etc/nginx/sites-available/paymenter.conf
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name localhost;
|
||||||
|
root /opt/paymenter/public;
|
||||||
|
|
||||||
|
index index.php;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files \$uri \$uri/ /index.php?\$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php\$ {
|
||||||
|
include snippets/fastcgi-php.conf;
|
||||||
|
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
|
||||||
|
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\.ht {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
ln -s /etc/nginx/sites-available/paymenter.conf /etc/nginx/sites-enabled/
|
||||||
|
rm -f /etc/nginx/sites-enabled/default
|
||||||
|
$STD systemctl reload nginx
|
||||||
|
chown -R www-data:www-data /opt/paymenter/*
|
||||||
|
msg_ok "Configured Nginx"
|
||||||
|
|
||||||
|
msg_info "Setting up Cronjob"
|
||||||
|
echo "* * * * * php /opt/paymenter/artisan schedule:run >> /dev/null 2>&1" | crontab -
|
||||||
|
msg_ok "Setup Cronjob"
|
||||||
|
|
||||||
|
msg_info "Setting up Service"
|
||||||
|
cat <<EOF >/etc/systemd/system/paymenter.service
|
||||||
|
[Unit]
|
||||||
|
Description=Paymenter Queue Worker
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=www-data
|
||||||
|
Group=www-data
|
||||||
|
Restart=always
|
||||||
|
ExecStart=/usr/bin/php /opt/paymenter/artisan queue:work
|
||||||
|
StartLimitInterval=180
|
||||||
|
StartLimitBurst=30
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
$STD systemctl enable --now paymenter.service
|
||||||
|
msg_ok "Setup Service"
|
||||||
|
|
||||||
|
msg_info "Cleaning up"
|
||||||
|
rm -rf /opt/paymenter/paymenter.tar.gz
|
||||||
|
$STD apt-get -y autoremove
|
||||||
|
$STD apt-get -y autoclean
|
||||||
|
msg_ok "Cleaned"
|
||||||
|
|
||||||
|
motd_ssh
|
||||||
|
customize
|
||||||
@@ -22,8 +22,8 @@ msg_ok "Installed Dependencies"
|
|||||||
RELEASE=$(curl -s https://api.github.com/repos/TriliumNext/Notes/releases/latest | grep "tag_name" | awk '{print substr($2, 2, length($2)-3) }')
|
RELEASE=$(curl -s https://api.github.com/repos/TriliumNext/Notes/releases/latest | grep "tag_name" | awk '{print substr($2, 2, length($2)-3) }')
|
||||||
|
|
||||||
msg_info "Installing TriliumNext"
|
msg_info "Installing TriliumNext"
|
||||||
wget -q https://github.com/TriliumNext/Notes/releases/download/${RELEASE}/TriliumNextNotes-${RELEASE}-server-linux-x64.tar.xz
|
wget -q https://github.com/TriliumNext/Notes/releases/download/${RELEASE}/TriliumNextNotes-linux-x64-${RELEASE}.tar.xz
|
||||||
tar -xf TriliumNextNotes-${RELEASE}-server-linux-x64.tar.xz
|
tar -xf TriliumNextNotes-linux-x64-${RELEASE}.tar.xz
|
||||||
mv trilium-linux-x64-server /opt/trilium
|
mv trilium-linux-x64-server /opt/trilium
|
||||||
msg_ok "Installed TriliumNext"
|
msg_ok "Installed TriliumNext"
|
||||||
|
|
||||||
@@ -53,5 +53,5 @@ customize
|
|||||||
msg_info "Cleaning up"
|
msg_info "Cleaning up"
|
||||||
$STD apt-get -y autoremove
|
$STD apt-get -y autoremove
|
||||||
$STD apt-get -y autoclean
|
$STD apt-get -y autoclean
|
||||||
rm -rf TriliumNextNotes-${RELEASE}-server-linux-x64.tar.xz
|
rm -rf TriliumNextNotes-linux-x64-${RELEASE}.tar.xz
|
||||||
msg_ok "Cleaned"
|
msg_ok "Cleaned"
|
||||||
|
|||||||
34
json/paymenter.json
Normal file
34
json/paymenter.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "Paymenter",
|
||||||
|
"slug": "paymenter",
|
||||||
|
"categories": [
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"date_created": "2025-01-28",
|
||||||
|
"type": "ct",
|
||||||
|
"updateable": true,
|
||||||
|
"privileged": false,
|
||||||
|
"interface_port": 80,
|
||||||
|
"documentation": "https://paymenter.org/docs",
|
||||||
|
"website": "https://paymenter.org/",
|
||||||
|
"logo": "https://avatars.githubusercontent.com/u/115177786?s=200&v=4",
|
||||||
|
"description": "Paymenter is an open source webshop solution for hosting companies. It's developed to provide an more easy way to manage your hosting company.",
|
||||||
|
"install_methods": [
|
||||||
|
{
|
||||||
|
"type": "default",
|
||||||
|
"script": "ct/paymenter.sh",
|
||||||
|
"resources": {
|
||||||
|
"cpu": 2,
|
||||||
|
"ram": 1024,
|
||||||
|
"hdd": 5,
|
||||||
|
"os":"Debian",
|
||||||
|
"version":"12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default_credentials": {
|
||||||
|
"username": "admin@paymenter.org",
|
||||||
|
"password": "paymenter"
|
||||||
|
},
|
||||||
|
"notes": []
|
||||||
|
}
|
||||||
@@ -2,8 +2,7 @@
|
|||||||
# Author: tteck (tteckster)
|
# Author: tteck (tteckster)
|
||||||
# Co-Author: MickLesk
|
# Co-Author: MickLesk
|
||||||
# Co-Author: michelroegl-brunner
|
# Co-Author: michelroegl-brunner
|
||||||
# License: MIT
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
# https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
|
||||||
|
|
||||||
variables() {
|
variables() {
|
||||||
NSAPP=$(echo ${APP,,} | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces.
|
NSAPP=$(echo ${APP,,} | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces.
|
||||||
@@ -73,7 +72,6 @@ error_handler() {
|
|||||||
local exit_code="$?"
|
local exit_code="$?"
|
||||||
local line_number="$1"
|
local line_number="$1"
|
||||||
local command="$2"
|
local command="$2"
|
||||||
post_update_to_api "failed"
|
|
||||||
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
|
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
|
||||||
echo -e "\n$error_message\n"
|
echo -e "\n$error_message\n"
|
||||||
}
|
}
|
||||||
@@ -794,6 +792,9 @@ advanced_settings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
post_to_api() {
|
post_to_api() {
|
||||||
|
if [ "$DIAGNOSTICS" = "no" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
local API_URL="http://api.community-scripts.org/upload"
|
local API_URL="http://api.community-scripts.org/upload"
|
||||||
local pve_version="not found"
|
local pve_version="not found"
|
||||||
pve_version=$(pveversion | awk -F'[/ ]' '{print $2}')
|
pve_version=$(pveversion | awk -F'[/ ]' '{print $2}')
|
||||||
@@ -831,13 +832,17 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
POST_UPDATE_DONE=false
|
POST_UPDATE_DONE=false
|
||||||
|
|
||||||
post_update_to_api() {
|
post_update_to_api() {
|
||||||
|
if [ "$DIAGNOSTICS" = "no" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
if [ "$POST_UPDATE_DONE" = true ]; then
|
if [ "$POST_UPDATE_DONE" = true ]; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local API_URL="http://api.community-scripts.org/upload/updatestatus"
|
local API_URL="http://api.community-scripts.org/upload/updatestatus"
|
||||||
local status="${1:-}"
|
local status="${1:-failed}"
|
||||||
|
|
||||||
JSON_PAYLOAD=$(cat <<EOF
|
JSON_PAYLOAD=$(cat <<EOF
|
||||||
{
|
{
|
||||||
"status": "$status",
|
"status": "$status",
|
||||||
@@ -857,6 +862,7 @@ EOF
|
|||||||
POST_UPDATE_DONE=true
|
POST_UPDATE_DONE=true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
diagnostics_check(){
|
diagnostics_check(){
|
||||||
if ! [ -d "/usr/local/community-scripts" ]; then
|
if ! [ -d "/usr/local/community-scripts" ]; then
|
||||||
mkdir -p /usr/local/community-scripts
|
mkdir -p /usr/local/community-scripts
|
||||||
@@ -1233,12 +1239,10 @@ EOF
|
|||||||
systemctl start ping-instances.service
|
systemctl start ping-instances.service
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $DIAGNOSTICS == "yes" ]]; then
|
post_update_to_api "done"
|
||||||
post_update_to_api
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trap 'post_update_to_api "done"' EXIT
|
trap 'post_update_to_api "failed"' EXIT
|
||||||
trap 'post_update_to_api "failed"' SIGINT
|
trap 'post_update_to_api "failed"' SIGINT
|
||||||
trap 'post_update_to_api "failed"' SIGTERM
|
trap 'post_update_to_api "failed"' SIGTERM
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user