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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
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 [selectedServer, setSelectedServer] = useState<Server | null>(null);
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const [pausingServers, setPausingServers] = useState<Set<string>>(new Set());
|
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 navigate = useNavigate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@@ -42,6 +46,25 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
|||||||
server.ip_address.toLowerCase().includes(searchTerm.toLowerCase())
|
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) => {
|
const handleViewDetails = (serverId: string) => {
|
||||||
navigate(`/server-detail/${serverId}`);
|
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 = ({
|
const CustomProgressBar = ({
|
||||||
value,
|
value,
|
||||||
label,
|
label,
|
||||||
@@ -234,6 +284,18 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
|||||||
className="pl-8"
|
className="pl-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<Button onClick={onRefresh} variant="outline" size="icon">
|
||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -250,6 +312,16 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader className={`${theme === 'dark' ? 'bg-gray-800' : 'bg-gray-50'}`}>
|
<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'}`}>
|
<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('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('status')}</TableHead>
|
||||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('OS')}</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 isPaused = server.status === "paused";
|
||||||
const isProcessing = pausingServers.has(server.id);
|
const isProcessing = pausingServers.has(server.id);
|
||||||
|
|
||||||
|
const isSelected = selectedServerIds.has(server.id);
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={server.id}
|
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)}
|
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">
|
<TableCell className="font-medium">
|
||||||
<div className="truncate" title={server.name}>
|
<div className="truncate" title={server.name}>
|
||||||
{server.name}
|
{server.name}
|
||||||
@@ -427,6 +507,30 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
|||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</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...",
|
loadingServers: "Loading servers...",
|
||||||
searchServersPlaceholder: "Search servers...",
|
searchServersPlaceholder: "Search servers...",
|
||||||
noServersFound: "No servers found",
|
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",
|
name: "Name",
|
||||||
status: "Status",
|
status: "Status",
|
||||||
OS: "OS",
|
OS: "OS",
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ export interface InstanceTranslations {
|
|||||||
loadingServers: string;
|
loadingServers: string;
|
||||||
searchServersPlaceholder: string;
|
searchServersPlaceholder: string;
|
||||||
noServersFound: string;
|
noServersFound: string;
|
||||||
|
deleteSelected: string;
|
||||||
|
deleteSelectedConfirmTitle: string;
|
||||||
|
deleteSelectedConfirmDesc: string;
|
||||||
|
selectedCount: string;
|
||||||
|
serversDeleted: string;
|
||||||
|
serversDeletedDesc: string;
|
||||||
|
partialSuccess: string;
|
||||||
name: string;
|
name: string;
|
||||||
status: string;
|
status: string;
|
||||||
OS: string;
|
OS: string;
|
||||||
|
|||||||
@@ -3,6 +3,15 @@ import type { Config } from "tailwindcss";
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
darkMode: ["class"],
|
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: [
|
content: [
|
||||||
"./pages/**/*.{ts,tsx}",
|
"./pages/**/*.{ts,tsx}",
|
||||||
"./components/**/*.{ts,tsx}",
|
"./components/**/*.{ts,tsx}",
|
||||||
|
|||||||
@@ -2,16 +2,38 @@ version: '3.9'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
checkcle:
|
checkcle:
|
||||||
build:
|
image: operacle/checkcle:latest
|
||||||
context: .
|
container_name: checkcle-dev
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: checkcle
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8090:8090"
|
- "8090:8090"
|
||||||
volumes:
|
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:
|
ulimits:
|
||||||
nofile:
|
nofile:
|
||||||
soft: 4096
|
soft: 4096
|
||||||
hard: 8192
|
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