Add auto refresh dropdown (#620)

This commit is contained in:
Saeed Vaziry
2025-06-21 22:17:49 +02:00
committed by GitHub
parent 736e27fa4e
commit dd14e69239
8 changed files with 107 additions and 9 deletions

View File

@ -76,6 +76,7 @@ public function share(Request $request): array
...parent::share($request),
...$data,
'name' => config('app.name'),
'version' => config('app.version'),
'quote' => ['message' => trim($message), 'author' => trim($author)],
'auth' => [
'user' => $user,

View File

@ -21,7 +21,7 @@ export default function AppCommand() {
return (
<div>
<Button className="px-1!" variant="outline" size="sm" onClick={() => setOpen(true)}>
<Button className="hidden px-1! lg:flex" variant="outline" size="sm" onClick={() => setOpen(true)}>
<span className="sr-only">Open command menu</span>
<SearchIcon className="ml-1 size-3" />
Search...
@ -29,6 +29,9 @@ export default function AppCommand() {
<CommandIcon className="mr-1 size-3" /> K
</span>
</Button>
<Button className="lg:hidden" variant="outline" size="sm" onClick={() => setOpen(true)}>
<CommandIcon className="mr-1 size-3" /> K
</Button>
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command or search..." />
<CommandList>

View File

@ -6,6 +6,7 @@ import AppCommand from '@/components/app-command';
import { SiteSwitch } from '@/components/site-switch';
import { usePage } from '@inertiajs/react';
import { SharedData } from '@/types';
import Refresh from '@/components/refresh';
export function AppHeader() {
const page = usePage<SharedData>();
@ -26,7 +27,10 @@ export function AppHeader() {
)}
</div>
</div>
<div className="flex items-center gap-2">
<AppCommand />
<Refresh />
</div>
</header>
);
}

View File

@ -12,12 +12,13 @@ import {
SidebarMenuSub,
SidebarMenuSubItem,
} from '@/components/ui/sidebar';
import { type NavItem } from '@/types';
import { Link } from '@inertiajs/react';
import { type NavItem, SharedData } from '@/types';
import { Link, usePage } from '@inertiajs/react';
import { BookOpen, ChevronRightIcon, CogIcon, Folder, MousePointerClickIcon, ServerIcon, ZapIcon } from 'lucide-react';
import AppLogo from './app-logo';
import { Icon } from '@/components/icon';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
const mainNavItems: NavItem[] = [
{
@ -56,6 +57,8 @@ const footerNavItems: NavItem[] = [
];
export function AppSidebar({ secondNavItems, secondNavTitle }: { secondNavItems?: NavItem[]; secondNavTitle?: string }) {
const page = usePage<SharedData>();
return (
<Sidebar collapsible="icon" className="overflow-hidden [&>[data-sidebar=sidebar]]:flex-row">
{/* This is the first sidebar */}
@ -67,7 +70,12 @@ export function AppSidebar({ secondNavItems, secondNavTitle }: { secondNavItems?
<SidebarMenuItem>
<SidebarMenuButton size="lg" asChild className="md:h-8 md:p-0">
<Link href={route('servers')} prefetch>
<Tooltip>
<TooltipTrigger>
<AppLogo />
</TooltipTrigger>
<TooltipContent side="right">{page.props.version}</TooltipContent>
</Tooltip>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>

View File

@ -0,0 +1,71 @@
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { Button } from '@/components/ui/button';
import { ChevronDownIcon, RefreshCwIcon } from 'lucide-react';
import { useEffect, useState } from 'react';
import { router } from '@inertiajs/react';
export default function Refresh() {
const [poll, setPoll] = useState<{
stop: VoidFunction;
start: VoidFunction;
}>();
const [polling, setPolling] = useState(false);
const storedInterval = (localStorage.getItem('refresh_interval') as '5' | '10' | '30' | '60' | '0') || '10';
const [refreshInterval, setRefreshInterval] = useState<5 | 10 | 30 | 60 | 0>(
storedInterval === '0' ? 0 : (parseInt(storedInterval) as 5 | 10 | 30 | 60),
);
const intervalLabels = {
5: '5s',
10: '10s',
30: '30s',
60: '1m',
0: 'OFF',
};
const refresh = () => {
router.reload({
onStart: () => {
setPolling(true);
},
onFinish: () => {
setPolling(false);
},
});
};
useEffect(() => {
poll?.stop();
if (refreshInterval > 0) {
setPoll(router.poll(refreshInterval * 1000));
} else {
poll?.stop();
setPoll(undefined);
}
localStorage.setItem('refresh_interval', refreshInterval.toString());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [refreshInterval]);
return (
<div className="flex items-center">
<Button variant="outline" size="sm" className="md:rounded-r-none" onClick={refresh} disabled={polling}>
{polling ? <RefreshCwIcon className="animate-spin" /> : <RefreshCwIcon className="lg:hidden" />}
<span className="hidden md:block">Refresh</span>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="hidden rounded-l-none border-l-0 md:flex">
{intervalLabels[refreshInterval] || 'Unknown'}
<ChevronDownIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={() => setRefreshInterval(5)}>5s</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setRefreshInterval(10)}>10s</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setRefreshInterval(30)}>30s</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setRefreshInterval(60)}>1m</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setRefreshInterval(0)}>OFF</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}

View File

@ -1,6 +1,7 @@
import AppLogoIcon from '@/components/app-logo-icon';
import { Link } from '@inertiajs/react';
import { Link, usePage } from '@inertiajs/react';
import { type PropsWithChildren } from 'react';
import { SharedData } from '@/types';
interface AuthLayoutProps {
name?: string;
@ -9,6 +10,7 @@ interface AuthLayoutProps {
}
export default function AuthLayout({ children, title, description }: PropsWithChildren<AuthLayoutProps>) {
const page = usePage<SharedData>();
return (
<div className="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div className="w-full max-w-sm">
@ -27,6 +29,16 @@ export default function AuthLayout({ children, title, description }: PropsWithCh
</div>
</div>
{children}
<div className="text-muted-foreground/50 text-center text-xs">
VitoDeploy{' '}
<a
href={`https://github.com/vitodeploy/vito/releases/tag/${page.props.version}`}
className="hover:text-primary cursor-pointer"
target="_blank"
>
{page.props.version}
</a>
</div>
</div>
</div>
</div>

View File

@ -26,15 +26,13 @@ import { ReactNode } from 'react';
import { Server } from '@/types/server';
import ServerHeader from '@/pages/servers/components/header';
import Layout from '@/layouts/app/layout';
import { usePage, usePoll } from '@inertiajs/react';
import { usePage } from '@inertiajs/react';
import { Site } from '@/types/site';
import PHPIcon from '@/icons/php';
import NodeIcon from '@/icons/node';
import siteHelper from '@/lib/site-helper';
export default function ServerLayout({ children }: { children: ReactNode }) {
usePoll(7000);
const page = usePage<{
server: Server;
site?: Site;

View File

@ -104,6 +104,7 @@ export interface Configs {
export interface SharedData {
name: string;
version: string;
quote: { message: string; author: string };
auth: Auth;
ziggy: Config & { location: string };