fix(config): добавить новые зависимости и обновить конфигурацию компонентов
Docker images / prepare-release (push) Successful in 5s
Docker images / backend-image (push) Successful in 2m37s
Docker images / frontend-image (push) Successful in 2m22s
Docker images / updater-image (push) Successful in 38s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 8s
Docker images / prepare-release (push) Successful in 5s
Docker images / backend-image (push) Successful in 2m37s
Docker images / frontend-image (push) Successful in 2m22s
Docker images / updater-image (push) Successful in 38s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 8s
This commit is contained in:
+27
-83
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useMemo, useEffect, useRef, useCallback } from "react"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import { FormField, FormToggle, SegmentedControl } from "@/components/form-kit"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
@@ -212,21 +213,6 @@ function probeGroupActionKey(srvId: string, group: { name: string; target: strin
|
||||
|
||||
// ── shared components ──────────────────────────────────────────────────────────
|
||||
|
||||
function Toggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
|
||||
return (
|
||||
<button type="button" onClick={() => onChange(!checked)}
|
||||
className={cn(
|
||||
"relative inline-flex h-5 w-9 shrink-0 rounded-full border-2 border-transparent transition-colors cursor-pointer",
|
||||
checked ? "bg-primary" : "bg-input",
|
||||
)}>
|
||||
<span className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-white shadow-sm transition-transform",
|
||||
checked ? "translate-x-4" : "translate-x-0",
|
||||
)} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function TypeChip({ type }: { type: "jump-host" | "exit-node" | "home-router" }) {
|
||||
return (
|
||||
<span className={cn(
|
||||
@@ -267,18 +253,6 @@ function StatChip({
|
||||
)
|
||||
}
|
||||
|
||||
function Field({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-medium">
|
||||
{label}
|
||||
{hint && <span className="font-normal text-muted-foreground ml-1">{hint}</span>}
|
||||
</label>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Активный интерфейс RouterOS: не disabled и running */
|
||||
function isActiveRouterOsInterface(i: { running?: boolean; disabled?: boolean }): boolean {
|
||||
return i.running === true && i.disabled !== true
|
||||
@@ -323,36 +297,6 @@ function interfaceOptionMatchesSearch(iface: RouterInterfaceOption, raw: string)
|
||||
return false
|
||||
}
|
||||
|
||||
function SegmentedControl<T extends string>({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
}: {
|
||||
value: T
|
||||
onChange: (v: T) => void
|
||||
options: Array<{ value: T; label: string }>
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-1 rounded-md border border-border bg-muted/40 p-0.5 w-fit">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => onChange(option.value)}
|
||||
className={cn(
|
||||
"px-3 py-1 text-sm rounded transition-colors",
|
||||
value === option.value
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ServerPickerCards({
|
||||
options,
|
||||
selectedId,
|
||||
@@ -2401,7 +2345,7 @@ export default function UptimePage() {
|
||||
className={cn("px-4 py-3 hover:bg-muted/20 transition-colors", !probe.enabled && "opacity-50")}>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
{/* enable toggle */}
|
||||
<Toggle checked={probe.enabled} onChange={(v) => updateSpeedProbe(probe.id, { enabled: v })} />
|
||||
<FormToggle checked={probe.enabled} onChange={(v) => updateSpeedProbe(probe.id, { enabled: v })} />
|
||||
|
||||
{/* route: src → dst */}
|
||||
<div className="flex items-center gap-1.5 min-w-0 flex-1">
|
||||
@@ -2824,7 +2768,7 @@ export default function UptimePage() {
|
||||
!p.enabled && "opacity-40",
|
||||
)}
|
||||
style={{ gridTemplateColumns: "36px 16px 130px 120px 140px 70px 44px minmax(132px,1fr) 96px 36px 72px" }}>
|
||||
<Toggle checked={p.enabled} onChange={v => toggleProbe(p.id, v)} />
|
||||
<FormToggle checked={p.enabled} onChange={v => toggleProbe(p.id, v)} />
|
||||
<StatusDot
|
||||
status={p.status === "up" ? "online" : p.status === "warn" ? "degraded" : "offline"}
|
||||
pulse={p.status === "up" && p.enabled}
|
||||
@@ -3002,7 +2946,7 @@ export default function UptimePage() {
|
||||
</SheetHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-5 py-5 flex flex-col gap-5">
|
||||
<Field label="Источник">
|
||||
<FormField label="Источник">
|
||||
<ServerPickerCards
|
||||
options={selectableSources}
|
||||
selectedId={speedDraft.srcServerId}
|
||||
@@ -3015,9 +2959,9 @@ export default function UptimePage() {
|
||||
void loadSpeedInterfaces(nextSrc)
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
<Field label="Назначение">
|
||||
<FormField label="Назначение">
|
||||
<ServerPickerCards
|
||||
options={selectableSources.filter((s) => s.id !== speedDraft.srcServerId)}
|
||||
selectedId={speedDraft.dstServerId}
|
||||
@@ -3026,28 +2970,28 @@ export default function UptimePage() {
|
||||
void loadSpeedInterfaces(nextDst)
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
<Field label="Интерфейс источника">
|
||||
<FormField label="Интерфейс источника">
|
||||
<InterfacePickerCards
|
||||
value={speedDraft.srcInterface}
|
||||
onChange={(v) => setSpeedDraft((prev) => ({ ...prev, srcInterface: v }))}
|
||||
options={filterActiveInterfaces(speedIfaces[speedDraft.srcServerId] ?? [])}
|
||||
autoLabel="auto"
|
||||
/>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
<Field label="Интерфейс назначения">
|
||||
<FormField label="Интерфейс назначения">
|
||||
<InterfacePickerCards
|
||||
value={speedDraft.dstInterface}
|
||||
onChange={(v) => setSpeedDraft((prev) => ({ ...prev, dstInterface: v }))}
|
||||
options={filterActiveInterfaces(speedIfaces[speedDraft.dstServerId] ?? [])}
|
||||
autoLabel="auto"
|
||||
/>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<Field label="Протокол">
|
||||
<FormField label="Протокол">
|
||||
<SegmentedControl
|
||||
value={speedDraft.protocol}
|
||||
onChange={(v) => setSpeedDraft((prev) => ({ ...prev, protocol: v }))}
|
||||
@@ -3056,8 +3000,8 @@ export default function UptimePage() {
|
||||
{ value: "udp", label: "UDP" },
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Direction">
|
||||
</FormField>
|
||||
<FormField label="Direction">
|
||||
<SegmentedControl
|
||||
value={speedDraft.direction}
|
||||
onChange={(v) => setSpeedDraft((prev) => ({ ...prev, direction: v }))}
|
||||
@@ -3067,10 +3011,10 @@ export default function UptimePage() {
|
||||
{ value: "receive", label: "rx" },
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Сек">
|
||||
</FormField>
|
||||
<FormField label="Сек">
|
||||
<Input className="h-9" value={speedDraft.durationSec} onChange={(e) => setSpeedDraft((prev) => ({ ...prev, durationSec: e.target.value }))} />
|
||||
</Field>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
{speedDraft.srcServerId &&
|
||||
@@ -3169,7 +3113,7 @@ export default function UptimePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Field
|
||||
<FormField
|
||||
label="Источник (кто пингует)"
|
||||
hint="— весь каталог, в т.ч. выключенные в инвентаре (Home Router часто «выкл.», но доступен по LAN для ping)"
|
||||
>
|
||||
@@ -3178,9 +3122,9 @@ export default function UptimePage() {
|
||||
selectedId={newSrcId}
|
||||
onSelect={(id) => setNewSrcId(id)}
|
||||
/>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
<Field label="Интерфейс источника" hint="(необязательно)">
|
||||
<FormField label="Интерфейс источника" hint="(необязательно)">
|
||||
<InterfacePickerCards
|
||||
value={newSrcInterface}
|
||||
onChange={setNewSrcInterface}
|
||||
@@ -3188,25 +3132,25 @@ export default function UptimePage() {
|
||||
autoLabel="авто (по маршруту)"
|
||||
busy={srcInterfacesBusy}
|
||||
/>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
<Field label="Имя пробы">
|
||||
<FormField label="Имя пробы">
|
||||
<Input className="h-9 text-sm" placeholder="youtube.com"
|
||||
value={newName} onChange={e => setNewName(e.target.value)} />
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
<Field label="Целевой IP / хост">
|
||||
<FormField label="Целевой IP / хост">
|
||||
<Input className="h-9 text-sm font-mono" placeholder="142.250.74.110"
|
||||
value={newTarget} onChange={e => setNewTarget(e.target.value)} />
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
<Field label="Связанный фильтр" hint="(необязательно)">
|
||||
<FormField label="Связанный фильтр" hint="(необязательно)">
|
||||
<LinkedFilterPickerCards
|
||||
value={newFilter}
|
||||
onChange={setNewFilter}
|
||||
items={filters}
|
||||
/>
|
||||
</Field>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<SheetFooter className="px-5 py-4 border-t shrink-0 gap-2">
|
||||
|
||||
Reference in New Issue
Block a user