mirror of
https://github.com/vitodeploy/vito.git
synced 2025-07-01 05:56:16 +00:00
Add auto refresh dropdown (#620)
This commit is contained in:
@ -76,6 +76,7 @@ public function share(Request $request): array
|
|||||||
...parent::share($request),
|
...parent::share($request),
|
||||||
...$data,
|
...$data,
|
||||||
'name' => config('app.name'),
|
'name' => config('app.name'),
|
||||||
|
'version' => config('app.version'),
|
||||||
'quote' => ['message' => trim($message), 'author' => trim($author)],
|
'quote' => ['message' => trim($message), 'author' => trim($author)],
|
||||||
'auth' => [
|
'auth' => [
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
|
@ -21,7 +21,7 @@ export default function AppCommand() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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>
|
<span className="sr-only">Open command menu</span>
|
||||||
<SearchIcon className="ml-1 size-3" />
|
<SearchIcon className="ml-1 size-3" />
|
||||||
Search...
|
Search...
|
||||||
@ -29,6 +29,9 @@ export default function AppCommand() {
|
|||||||
<CommandIcon className="mr-1 size-3" /> K
|
<CommandIcon className="mr-1 size-3" /> K
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</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}>
|
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||||
<CommandInput placeholder="Type a command or search..." />
|
<CommandInput placeholder="Type a command or search..." />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
|
@ -6,6 +6,7 @@ import AppCommand from '@/components/app-command';
|
|||||||
import { SiteSwitch } from '@/components/site-switch';
|
import { SiteSwitch } from '@/components/site-switch';
|
||||||
import { usePage } from '@inertiajs/react';
|
import { usePage } from '@inertiajs/react';
|
||||||
import { SharedData } from '@/types';
|
import { SharedData } from '@/types';
|
||||||
|
import Refresh from '@/components/refresh';
|
||||||
|
|
||||||
export function AppHeader() {
|
export function AppHeader() {
|
||||||
const page = usePage<SharedData>();
|
const page = usePage<SharedData>();
|
||||||
@ -26,7 +27,10 @@ export function AppHeader() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AppCommand />
|
<div className="flex items-center gap-2">
|
||||||
|
<AppCommand />
|
||||||
|
<Refresh />
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,13 @@ import {
|
|||||||
SidebarMenuSub,
|
SidebarMenuSub,
|
||||||
SidebarMenuSubItem,
|
SidebarMenuSubItem,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
import { type NavItem } from '@/types';
|
import { type NavItem, SharedData } from '@/types';
|
||||||
import { Link } from '@inertiajs/react';
|
import { Link, usePage } from '@inertiajs/react';
|
||||||
import { BookOpen, ChevronRightIcon, CogIcon, Folder, MousePointerClickIcon, ServerIcon, ZapIcon } from 'lucide-react';
|
import { BookOpen, ChevronRightIcon, CogIcon, Folder, MousePointerClickIcon, ServerIcon, ZapIcon } from 'lucide-react';
|
||||||
import AppLogo from './app-logo';
|
import AppLogo from './app-logo';
|
||||||
import { Icon } from '@/components/icon';
|
import { Icon } from '@/components/icon';
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
const mainNavItems: NavItem[] = [
|
const mainNavItems: NavItem[] = [
|
||||||
{
|
{
|
||||||
@ -56,6 +57,8 @@ const footerNavItems: NavItem[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function AppSidebar({ secondNavItems, secondNavTitle }: { secondNavItems?: NavItem[]; secondNavTitle?: string }) {
|
export function AppSidebar({ secondNavItems, secondNavTitle }: { secondNavItems?: NavItem[]; secondNavTitle?: string }) {
|
||||||
|
const page = usePage<SharedData>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon" className="overflow-hidden [&>[data-sidebar=sidebar]]:flex-row">
|
<Sidebar collapsible="icon" className="overflow-hidden [&>[data-sidebar=sidebar]]:flex-row">
|
||||||
{/* This is the first sidebar */}
|
{/* This is the first sidebar */}
|
||||||
@ -67,7 +70,12 @@ export function AppSidebar({ secondNavItems, secondNavTitle }: { secondNavItems?
|
|||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton size="lg" asChild className="md:h-8 md:p-0">
|
<SidebarMenuButton size="lg" asChild className="md:h-8 md:p-0">
|
||||||
<Link href={route('servers')} prefetch>
|
<Link href={route('servers')} prefetch>
|
||||||
<AppLogo />
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<AppLogo />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">{page.props.version}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
71
resources/js/components/refresh.tsx
Normal file
71
resources/js/components/refresh.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import AppLogoIcon from '@/components/app-logo-icon';
|
import AppLogoIcon from '@/components/app-logo-icon';
|
||||||
import { Link } from '@inertiajs/react';
|
import { Link, usePage } from '@inertiajs/react';
|
||||||
import { type PropsWithChildren } from 'react';
|
import { type PropsWithChildren } from 'react';
|
||||||
|
import { SharedData } from '@/types';
|
||||||
|
|
||||||
interface AuthLayoutProps {
|
interface AuthLayoutProps {
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -9,6 +10,7 @@ interface AuthLayoutProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AuthLayout({ children, title, description }: PropsWithChildren<AuthLayoutProps>) {
|
export default function AuthLayout({ children, title, description }: PropsWithChildren<AuthLayoutProps>) {
|
||||||
|
const page = usePage<SharedData>();
|
||||||
return (
|
return (
|
||||||
<div className="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
<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">
|
<div className="w-full max-w-sm">
|
||||||
@ -27,6 +29,16 @@ export default function AuthLayout({ children, title, description }: PropsWithCh
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,15 +26,13 @@ import { ReactNode } from 'react';
|
|||||||
import { Server } from '@/types/server';
|
import { Server } from '@/types/server';
|
||||||
import ServerHeader from '@/pages/servers/components/header';
|
import ServerHeader from '@/pages/servers/components/header';
|
||||||
import Layout from '@/layouts/app/layout';
|
import Layout from '@/layouts/app/layout';
|
||||||
import { usePage, usePoll } from '@inertiajs/react';
|
import { usePage } from '@inertiajs/react';
|
||||||
import { Site } from '@/types/site';
|
import { Site } from '@/types/site';
|
||||||
import PHPIcon from '@/icons/php';
|
import PHPIcon from '@/icons/php';
|
||||||
import NodeIcon from '@/icons/node';
|
import NodeIcon from '@/icons/node';
|
||||||
import siteHelper from '@/lib/site-helper';
|
import siteHelper from '@/lib/site-helper';
|
||||||
|
|
||||||
export default function ServerLayout({ children }: { children: ReactNode }) {
|
export default function ServerLayout({ children }: { children: ReactNode }) {
|
||||||
usePoll(7000);
|
|
||||||
|
|
||||||
const page = usePage<{
|
const page = usePage<{
|
||||||
server: Server;
|
server: Server;
|
||||||
site?: Site;
|
site?: Site;
|
||||||
|
1
resources/js/types/index.d.ts
vendored
1
resources/js/types/index.d.ts
vendored
@ -104,6 +104,7 @@ export interface Configs {
|
|||||||
|
|
||||||
export interface SharedData {
|
export interface SharedData {
|
||||||
name: string;
|
name: string;
|
||||||
|
version: string;
|
||||||
quote: { message: string; author: string };
|
quote: { message: string; author: string };
|
||||||
auth: Auth;
|
auth: Auth;
|
||||||
ziggy: Config & { location: string };
|
ziggy: Config & { location: string };
|
||||||
|
Reference in New Issue
Block a user