chore(dev): persist pocketbase data volume in dev compose and docs
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
.git
|
||||
server/
|
||||
**/node_modules
|
||||
**/dist
|
||||
**/.next
|
||||
**/.turbo
|
||||
**/.cache
|
||||
**/coverage
|
||||
**/*.log
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
name: Build and Publish Frontend Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "develop", "feature/**" ]
|
||||
paths:
|
||||
- 'application/**'
|
||||
- '.github/workflows/build-frontend-image.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/checkcle-frontend
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=sha
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: application
|
||||
file: application/Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
provenance: false
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
FROM node:20-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY application/package.json application/package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Copy source and build
|
||||
COPY application/. .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.27-alpine
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
# SPA fallback + static asset caching
|
||||
RUN printf 'server {\n listen 80;\n server_name _;\n root /usr/share/nginx/html;\n index index.html;\n location / { try_files $uri $uri/ /index.html; }\n location ~* \\.(?:js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ { expires 7d; add_header Cache-Control "public, max-age=604800, immutable"; try_files $uri =404; }\n}\n' > /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
CMD ["nginx","-g","daemon off;"]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.git
|
||||
node_modules
|
||||
dist
|
||||
.cache
|
||||
*.log
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
FROM node:20-bullseye-slim AS build
|
||||
WORKDIR /app
|
||||
ENV npm_config_fund=false \
|
||||
npm_config_audit=false \
|
||||
npm_config_progress=false
|
||||
COPY package.json package-lock.json ./
|
||||
RUN --mount=type=cache,target=/root/.npm npm ci --no-audit --fund=false
|
||||
COPY . .
|
||||
RUN --mount=type=cache,target=/root/.npm npm run build
|
||||
|
||||
FROM nginx:1.27-alpine
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
# SPA fallback + asset caching
|
||||
RUN printf 'server {\n listen 80;\n server_name _;\n root /usr/share/nginx/html;\n index index.html;\n location / { try_files $uri $uri/ /index.html; }\n location ~* \\.(?:js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ { expires 7d; add_header Cache-Control "public, max-age=604800, immutable"; try_files $uri =404; }\n}\n' > /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx","-g","daemon off;"]
|
||||
@@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
@@ -33,6 +34,9 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
const [selectedServer, setSelectedServer] = useState<Server | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [pausingServers, setPausingServers] = useState<Set<string>>(new Set());
|
||||
const [selectedServerIds, setSelectedServerIds] = useState<Set<string>>(new Set());
|
||||
const [bulkDeleteDialogOpen, setBulkDeleteDialogOpen] = useState(false);
|
||||
const [isBulkDeleting, setIsBulkDeleting] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -42,6 +46,25 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
server.ip_address.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const allVisibleSelected = filteredServers.length > 0 && filteredServers.every(s => selectedServerIds.has(s.id));
|
||||
const someVisibleSelected = filteredServers.some(s => selectedServerIds.has(s.id)) && !allVisibleSelected;
|
||||
|
||||
const toggleSelectAllVisible = (checked: boolean) => {
|
||||
const newSet = new Set(selectedServerIds);
|
||||
if (checked) {
|
||||
filteredServers.forEach(s => newSet.add(s.id));
|
||||
} else {
|
||||
filteredServers.forEach(s => newSet.delete(s.id));
|
||||
}
|
||||
setSelectedServerIds(newSet);
|
||||
};
|
||||
|
||||
const toggleSelectOne = (serverId: string, checked: boolean) => {
|
||||
const newSet = new Set(selectedServerIds);
|
||||
if (checked) newSet.add(serverId); else newSet.delete(serverId);
|
||||
setSelectedServerIds(newSet);
|
||||
};
|
||||
|
||||
const handleViewDetails = (serverId: string) => {
|
||||
navigate(`/server-detail/${serverId}`);
|
||||
};
|
||||
@@ -138,6 +161,33 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
}
|
||||
};
|
||||
|
||||
const confirmBulkDelete = async () => {
|
||||
if (selectedServerIds.size === 0 || isBulkDeleting) return;
|
||||
try {
|
||||
setIsBulkDeleting(true);
|
||||
const ids = Array.from(selectedServerIds);
|
||||
const deletions = ids.map(id => pb.collection('servers').delete(id));
|
||||
const results = await Promise.allSettled(deletions);
|
||||
const failed = results.filter(r => r.status === 'rejected').length;
|
||||
|
||||
if (failed === 0) {
|
||||
toast({ title: "Servers deleted", description: `${ids.length} server(s) have been deleted.` });
|
||||
} else if (failed === ids.length) {
|
||||
toast({ variant: "destructive", title: "Error", description: "Failed to delete selected servers. Please try again." });
|
||||
} else {
|
||||
toast({ variant: "destructive", title: "Partial success", description: `Deleted ${ids.length - failed}, failed ${failed}.` });
|
||||
}
|
||||
|
||||
onRefresh();
|
||||
setSelectedServerIds(new Set());
|
||||
setBulkDeleteDialogOpen(false);
|
||||
} catch (_e) {
|
||||
toast({ variant: "destructive", title: "Error", description: "Failed to delete selected servers. Please try again." });
|
||||
} finally {
|
||||
setIsBulkDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const CustomProgressBar = ({
|
||||
value,
|
||||
label,
|
||||
@@ -234,6 +284,18 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
{selectedServerIds.size > 0 && (
|
||||
<div className="hidden sm:block text-sm text-muted-foreground mr-2">
|
||||
{selectedServerIds.size} selected
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => setBulkDeleteDialogOpen(true)}
|
||||
variant="destructive"
|
||||
disabled={selectedServerIds.size === 0}
|
||||
>
|
||||
Delete Selected
|
||||
</Button>
|
||||
<Button onClick={onRefresh} variant="outline" size="icon">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -250,6 +312,16 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
<Table>
|
||||
<TableHeader className={`${theme === 'dark' ? 'bg-gray-800' : 'bg-gray-50'}`}>
|
||||
<TableRow className={`${theme === 'dark' ? 'border-gray-700 hover:bg-gray-800' : 'border-gray-200 hover:bg-gray-100'}`}>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} w-10`}>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox
|
||||
checked={allVisibleSelected}
|
||||
onCheckedChange={(v) => toggleSelectAllVisible(Boolean(v))}
|
||||
aria-label="Select all"
|
||||
indeterminate={someVisibleSelected}
|
||||
/>
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('name')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('status')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('OS')}</TableHead>
|
||||
@@ -270,12 +342,20 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
const isPaused = server.status === "paused";
|
||||
const isProcessing = pausingServers.has(server.id);
|
||||
|
||||
const isSelected = selectedServerIds.has(server.id);
|
||||
return (
|
||||
<TableRow
|
||||
key={server.id}
|
||||
className="hover:bg-muted/50 cursor-pointer"
|
||||
className={`hover:bg-muted/50 cursor-pointer ${isSelected ? 'bg-muted/30' : ''}`}
|
||||
onClick={() => handleViewDetails(server.id)}
|
||||
>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={(v) => toggleSelectOne(server.id, Boolean(v))}
|
||||
aria-label={`Select ${server.name}`}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
<div className="truncate" title={server.name}>
|
||||
{server.name}
|
||||
@@ -427,6 +507,30 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* Bulk Delete Confirmation Dialog */}
|
||||
<AlertDialog open={bulkDeleteDialogOpen} onOpenChange={setBulkDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete selected servers?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete {selectedServerIds.size} server(s) and all of their monitoring data.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isBulkDeleting}>
|
||||
{t('cancel')}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmBulkDelete}
|
||||
disabled={isBulkDeleting}
|
||||
className="bg-red-600 text-white hover:bg-red-700"
|
||||
>
|
||||
{isBulkDeleting ? t('deleting') : 'Delete Selected'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,6 +18,13 @@ export const instanceTranslations: InstanceTranslations = {
|
||||
loadingServers: "Loading servers...",
|
||||
searchServersPlaceholder: "Search servers...",
|
||||
noServersFound: "No servers found",
|
||||
deleteSelected: "Delete Selected",
|
||||
deleteSelectedConfirmTitle: "Delete selected servers?",
|
||||
deleteSelectedConfirmDesc: "This action cannot be undone. This will permanently delete {count} server(s) and all of their monitoring data.",
|
||||
selectedCount: "{count} selected",
|
||||
serversDeleted: "Servers deleted",
|
||||
serversDeletedDesc: "{count} server(s) have been deleted.",
|
||||
partialSuccess: "Partial success",
|
||||
name: "Name",
|
||||
status: "Status",
|
||||
OS: "OS",
|
||||
|
||||
@@ -14,6 +14,13 @@ export interface InstanceTranslations {
|
||||
loadingServers: string;
|
||||
searchServersPlaceholder: string;
|
||||
noServersFound: string;
|
||||
deleteSelected: string;
|
||||
deleteSelectedConfirmTitle: string;
|
||||
deleteSelectedConfirmDesc: string;
|
||||
selectedCount: string;
|
||||
serversDeleted: string;
|
||||
serversDeletedDesc: string;
|
||||
partialSuccess: string;
|
||||
name: string;
|
||||
status: string;
|
||||
OS: string;
|
||||
|
||||
@@ -3,6 +3,15 @@ import type { Config } from "tailwindcss";
|
||||
|
||||
export default {
|
||||
darkMode: ["class"],
|
||||
safelist: [
|
||||
"text-purple-400",
|
||||
"text-blue-400",
|
||||
"text-cyan-400",
|
||||
"text-emerald-400",
|
||||
"text-amber-400",
|
||||
"text-indigo-400",
|
||||
"text-rose-400"
|
||||
],
|
||||
content: [
|
||||
"./pages/**/*.{ts,tsx}",
|
||||
"./components/**/*.{ts,tsx}",
|
||||
|
||||
@@ -2,16 +2,38 @@ version: '3.9'
|
||||
|
||||
services:
|
||||
checkcle:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: checkcle
|
||||
image: operacle/checkcle:latest
|
||||
container_name: checkcle-dev
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8090:8090"
|
||||
volumes:
|
||||
- /var/pb_data:/mnt/pb_data # Updated mount target to match CMD in Dockerfile
|
||||
- pb_data:/mnt/pb_data # Persist PocketBase data across rebuilds
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 4096
|
||||
hard: 8192
|
||||
|
||||
frontend:
|
||||
image: ghcr.io/diptamahardhika/checkcle-frontend:sha-b2ae075
|
||||
container_name: checkcle-frontend
|
||||
ports:
|
||||
- "8990:80"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
depends_on:
|
||||
- checkcle
|
||||
# Fall back to dev server when FRONTEND_IMAGE is not set
|
||||
deploy:
|
||||
replicas: 1
|
||||
# Compose doesn't support conditional configs; document usage:
|
||||
# - To use prebuilt image: export FRONTEND_IMAGE=ghcr.io/<owner>/checkcle-frontend:<tag>
|
||||
# - To use live dev server instead, comment 'image' above and uncomment the block below:
|
||||
# working_dir: /app
|
||||
# command: sh -c "npm ci && npm run dev -- --host 0.0.0.0 --port 8990"
|
||||
# volumes:
|
||||
# - ../application:/app
|
||||
# - /app/node_modules
|
||||
|
||||
volumes:
|
||||
pb_data:
|
||||
|
||||
Reference in New Issue
Block a user