feat(web): добавить поддержку быстрого доступа на дашборде и обновить настройки интерфейса
Docker / build (push) Failing after 19s

- Внедрён компонент QuickActionGrid для отображения часто используемых разделов на дашборде, управляемый настройкой showQuickActions.
- Добавлен новый параметр showQuickActions в схему настроек для управления видимостью быстрого доступа.
- Обновлён интерфейс настроек для включения переключателя отображения быстрого доступа.
- Добавлен компонент SystemMonitorPopover в заголовок приложения для мониторинга системы.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-17 20:32:59 +07:00
co-authored by Cursor
parent 9c43b45245
commit d52c633d90
15 changed files with 608 additions and 6 deletions
+16 -1
View File
@@ -44,6 +44,7 @@ export type SettingsDto = Omit<
| 'integrationEnabled'
| 'customFields'
| 'appSwitcherJson'
| 'showQuickActions'
> & {
telegramBotTokenSet: boolean
integrationTokenSet: boolean
@@ -56,6 +57,7 @@ export type SettingsDto = Omit<
notifyVpsDownEnabled: boolean
webhookEnabled: boolean
integrationEnabled: boolean
showQuickActions: boolean
notifyIntervalMinutes: number
uptimeCheckIntervalMinutes: number
customFields: unknown[]
@@ -81,7 +83,8 @@ function toDto(row: Row | undefined): SettingsDto | undefined {
customFields = []
}
}
const { telegramBotToken, integrationToken, appSwitcherJson, ...rest } = row
const { telegramBotToken, integrationToken, appSwitcherJson, showQuickActions: _showQa, ...rest } =
row
return {
...rest,
telegramBotTokenSet: Boolean(telegramBotToken?.trim()),
@@ -95,6 +98,7 @@ function toDto(row: Row | undefined): SettingsDto | undefined {
notifyVpsDownEnabled: Boolean(row.notifyVpsDownEnabled),
webhookEnabled: Boolean(row.webhookEnabled),
integrationEnabled: Boolean(row.integrationEnabled),
showQuickActions: row.showQuickActions == null ? true : Boolean(row.showQuickActions),
notifyIntervalMinutes: Number(row.notifyIntervalMinutes) || 60,
uptimeCheckIntervalMinutes: Number(row.uptimeCheckIntervalMinutes) || 5,
customFields: Array.isArray(customFields) ? customFields : [],
@@ -135,6 +139,7 @@ interface SettingsInput {
integrationEnabled?: boolean
integrationLastSyncAt?: string
cfdmApiUrl?: string
showQuickActions?: boolean
}
function buildValues(id: string, existing: Row | undefined, r: SettingsInput) {
@@ -238,6 +243,16 @@ function buildValues(id: string, existing: Row | undefined, r: SettingsInput) {
? r.integrationLastSyncAt || ''
: existing?.integrationLastSyncAt ?? '',
cfdmApiUrl: r.cfdmApiUrl !== undefined ? r.cfdmApiUrl || '' : existing?.cfdmApiUrl ?? '',
showQuickActions:
r.showQuickActions !== undefined
? r.showQuickActions
? 1
: 0
: existing?.showQuickActions == null
? 1
: existing.showQuickActions
? 1
: 0,
}
}
+1
View File
@@ -14,6 +14,7 @@ const COLUMN_MIGRATIONS: string[] = [
`ALTER TABLE settings ADD COLUMN integrationEnabled INTEGER`,
`ALTER TABLE settings ADD COLUMN integrationLastSyncAt TEXT`,
`ALTER TABLE settings ADD COLUMN cfdmApiUrl TEXT`,
`ALTER TABLE settings ADD COLUMN showQuickActions INTEGER`,
`ALTER TABLE vps_domains ADD COLUMN targetIps TEXT`,
]
+1
View File
@@ -133,6 +133,7 @@ export const settings = sqliteTable('settings', {
integrationEnabled: integer('integrationEnabled'),
integrationLastSyncAt: text('integrationLastSyncAt'),
cfdmApiUrl: text('cfdmApiUrl'),
showQuickActions: integer('showQuickActions'),
})
export const vpsDomains = sqliteTable('vps_domains', {
@@ -27,6 +27,7 @@ export const settingsSchema = z.object({
appSwitcher: appSwitcherConfigSchema.optional(),
integrationToken: z.string().optional(),
integrationEnabled: z.boolean().optional(),
showQuickActions: z.boolean().optional(),
})
export type Settings = z.infer<typeof settingsSchema>
+81
View File
@@ -0,0 +1,81 @@
import { Progress as ProgressPrimitive } from "@base-ui/react/progress"
import { cn } from "@cfdm/ui/lib/utils"
function Progress({
className,
children,
value,
...props
}: ProgressPrimitive.Root.Props) {
return (
<ProgressPrimitive.Root
value={value}
data-slot="progress"
className={cn("flex flex-wrap gap-3", className)}
{...props}
>
{children}
<ProgressTrack>
<ProgressIndicator />
</ProgressTrack>
</ProgressPrimitive.Root>
)
}
function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) {
return (
<ProgressPrimitive.Track
className={cn(
"relative flex h-1 w-full items-center overflow-x-hidden rounded-full bg-muted",
className
)}
data-slot="progress-track"
{...props}
/>
)
}
function ProgressIndicator({
className,
...props
}: ProgressPrimitive.Indicator.Props) {
return (
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className={cn("h-full bg-primary transition-all", className)}
{...props}
/>
)
}
function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) {
return (
<ProgressPrimitive.Label
className={cn("text-sm font-medium", className)}
data-slot="progress-label"
{...props}
/>
)
}
function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) {
return (
<ProgressPrimitive.Value
className={cn(
"ml-auto text-sm text-muted-foreground tabular-nums",
className
)}
data-slot="progress-value"
{...props}
/>
)
}
export {
Progress,
ProgressTrack,
ProgressIndicator,
ProgressLabel,
ProgressValue,
}
+32
View File
@@ -0,0 +1,32 @@
"use client"
import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
import { cn } from "@cfdm/ui/lib/utils"
function Switch({
className,
size = "default",
...props
}: SwitchPrimitive.Root.Props & {
size?: "sm" | "default"
}) {
return (
<SwitchPrimitive.Root
data-slot="switch"
data-size={size}
className={cn(
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
/>
</SwitchPrimitive.Root>
)
}
export { Switch }