Compare commits
3
Commits
9f00dfcf84
...
v1.11.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d063323402 | ||
|
|
6bced71037 | ||
|
|
44d0eb0114 |
No files matched your search
@@ -38,14 +38,11 @@ jobs:
|
|||||||
api: ${{ steps.detect.outputs.api }}
|
api: ${{ steps.detect.outputs.api }}
|
||||||
docker: ${{ steps.detect.outputs.docker }}
|
docker: ${{ steps.detect.outputs.docker }}
|
||||||
steps:
|
steps:
|
||||||
- if: ${{ inputs.is_pull_request }}
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
with:
|
||||||
|
# Push may contain several commits; github.event.before is then
|
||||||
|
# more than one parent away. fetch-depth: 2 only has HEAD~1.
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- if: ${{ inputs.is_pull_request == false }}
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
- id: detect
|
- id: detect
|
||||||
name: Detect changed paths per module
|
name: Detect changed paths per module
|
||||||
env:
|
env:
|
||||||
@@ -72,12 +69,16 @@ jobs:
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
has_commit() {
|
||||||
|
git cat-file -e "${1}^{commit}" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
if [ "$IS_PR" = "true" ]; then
|
if [ "$IS_PR" = "true" ]; then
|
||||||
FILES="$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")"
|
FILES="$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")"
|
||||||
else
|
else
|
||||||
after="${HEAD_SHA:-$(git rev-parse HEAD)}"
|
after="${HEAD_SHA:-$(git rev-parse HEAD)}"
|
||||||
before="$BEFORE_SHA"
|
before="$BEFORE_SHA"
|
||||||
if [ -n "$before" ] && [ "$before" != "0000000000000000000000000000000000000000" ]; then
|
if [ -n "$before" ] && [ "$before" != "0000000000000000000000000000000000000000" ] && has_commit "$before"; then
|
||||||
FILES="$(git diff --name-only "$before" "$after")"
|
FILES="$(git diff --name-only "$before" "$after")"
|
||||||
elif git rev-parse --verify HEAD~1 >/dev/null 2>&1; then
|
elif git rev-parse --verify HEAD~1 >/dev/null 2>&1; then
|
||||||
FILES="$(git diff --name-only HEAD~1 HEAD)"
|
FILES="$(git diff --name-only HEAD~1 HEAD)"
|
||||||
|
|||||||
@@ -312,6 +312,14 @@ async function buildView(db: Db, serviceId: number): Promise<ServiceView> {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const activeIps = new Set<string>();
|
||||||
|
for (const binding of bindings) {
|
||||||
|
const { config, rows } = getBindingLbState(db, binding.id);
|
||||||
|
for (const ip of selectActiveIpsByMode(config, rows)) {
|
||||||
|
activeIps.add(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: service.id,
|
id: service.id,
|
||||||
name: service.name,
|
name: service.name,
|
||||||
@@ -330,6 +338,8 @@ async function buildView(db: Db, serviceId: number): Promise<ServiceView> {
|
|||||||
health_status: "unknown",
|
health_status: "unknown",
|
||||||
health_latency_ms: null,
|
health_latency_ms: null,
|
||||||
ip_health: [],
|
ip_health: [],
|
||||||
|
lb_mode: bindings[0]?.lb_mode ?? "round_robin",
|
||||||
|
active_ips: [...activeIps],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -123,11 +123,12 @@ describe("create service then list groups", () => {
|
|||||||
expect(httpParsed.success, JSON.stringify(httpParsed.error?.issues)).toBe(
|
expect(httpParsed.success, JSON.stringify(httpParsed.error?.issues)).toBe(
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
expect(
|
const listed = httpParsed.data!.groups
|
||||||
httpParsed.data!.groups
|
.find((g) => g.id === group.id)
|
||||||
.find((g) => g.id === group.id)
|
?.services.find((s) => s.id === created.id);
|
||||||
?.services.some((s) => s.id === created.id),
|
expect(listed).toBeDefined();
|
||||||
).toBe(true);
|
expect(listed?.lb_mode).toBe("round_robin");
|
||||||
|
expect(listed?.active_ips).toEqual(["1.2.3.4"]);
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,12 +2,20 @@ import { CheckIcon, CopyIcon } from 'lucide-react'
|
|||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import { HealthCheckBadge } from '@/components/health-check-badge'
|
import { HealthCheckBadge } from '@/components/health-check-badge'
|
||||||
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
import { Badge } from '@/components/reui/badge'
|
import { Badge } from '@/components/reui/badge'
|
||||||
import { TruncatedText } from '@/components/truncated-text'
|
import { TruncatedText } from '@/components/truncated-text'
|
||||||
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
||||||
import { serviceDisplayFqdns } from '@/lib/service-utils'
|
import { serviceDisplayFqdns } from '@/lib/service-utils'
|
||||||
import type { ServiceView } from '@/lib/schemas'
|
import type { ServiceView } from '@/lib/schemas'
|
||||||
import { Button } from '@cfdm/ui/components/button'
|
import { Button } from '@cfdm/ui/components/button'
|
||||||
|
import {
|
||||||
|
Item,
|
||||||
|
ItemActions,
|
||||||
|
ItemContent,
|
||||||
|
ItemGroup,
|
||||||
|
ItemMedia,
|
||||||
|
} from '@cfdm/ui/components/item'
|
||||||
import { Switch } from '@cfdm/ui/components/switch'
|
import { Switch } from '@cfdm/ui/components/switch'
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -17,6 +25,9 @@ import {
|
|||||||
} from '@cfdm/ui/components/tooltip'
|
} from '@cfdm/ui/components/tooltip'
|
||||||
import { cn } from '@cfdm/ui/lib/utils'
|
import { cn } from '@cfdm/ui/lib/utils'
|
||||||
|
|
||||||
|
/** Matches `Button size="icon-sm"` so Switch columns align with the card menu. */
|
||||||
|
const MENU_SLOT_CLASS = 'size-7 shrink-0'
|
||||||
|
|
||||||
export function CopyFqdnButton({
|
export function CopyFqdnButton({
|
||||||
value,
|
value,
|
||||||
className,
|
className,
|
||||||
@@ -126,6 +137,11 @@ interface ServiceIpListProps {
|
|||||||
togglingIp?: string | null
|
togglingIp?: string | null
|
||||||
ipToggleDisabled?: boolean
|
ipToggleDisabled?: boolean
|
||||||
onToggleIp?: (ip: string, enabled: boolean) => void
|
onToggleIp?: (ip: string, enabled: boolean) => void
|
||||||
|
/** Invisible icon-sm slot so IP Switch lines up with the card overflow menu. */
|
||||||
|
alignWithMenu?: boolean
|
||||||
|
lbMode?: ServiceView['lb_mode']
|
||||||
|
activeIps?: string[]
|
||||||
|
ipWeights?: Record<string, number>
|
||||||
className?: string
|
className?: string
|
||||||
emptyLabel?: string
|
emptyLabel?: string
|
||||||
copyable?: boolean
|
copyable?: boolean
|
||||||
@@ -139,6 +155,10 @@ export function ServiceIpList({
|
|||||||
togglingIp = null,
|
togglingIp = null,
|
||||||
ipToggleDisabled = false,
|
ipToggleDisabled = false,
|
||||||
onToggleIp,
|
onToggleIp,
|
||||||
|
alignWithMenu = false,
|
||||||
|
lbMode,
|
||||||
|
activeIps = [],
|
||||||
|
ipWeights = {},
|
||||||
className,
|
className,
|
||||||
emptyLabel = 'Нет IP',
|
emptyLabel = 'Нет IP',
|
||||||
copyable = false,
|
copyable = false,
|
||||||
@@ -155,49 +175,83 @@ export function ServiceIpList({
|
|||||||
const healthByIp = new Map(ipHealth.map((row) => [row.ip, row]))
|
const healthByIp = new Map(ipHealth.map((row) => [row.ip, row]))
|
||||||
const visible = onToggleIp ? ips : ips.slice(0, VISIBLE_IP_LIMIT)
|
const visible = onToggleIp ? ips : ips.slice(0, VISIBLE_IP_LIMIT)
|
||||||
const extraCount = ips.length - visible.length
|
const extraCount = ips.length - visible.length
|
||||||
|
const showMenuSlot = Boolean(onToggleIp && alignWithMenu)
|
||||||
|
const markActive = lbMode === 'failover' || lbMode === 'weighted'
|
||||||
|
const activeSet = new Set(activeIps)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex min-w-0 flex-col gap-1', className)}>
|
<ItemGroup className={cn('gap-1', className)}>
|
||||||
{visible.map((ip) => {
|
{visible.map((ip) => {
|
||||||
const health = healthByIp.get(ip)
|
const health = healthByIp.get(ip)
|
||||||
const enabled = ipEnabled[ip] !== false
|
const enabled = ipEnabled[ip] !== false
|
||||||
return (
|
return (
|
||||||
<div key={ip} className="flex min-w-0 items-center gap-1.5">
|
<Item
|
||||||
<HealthCheckBadge
|
key={ip}
|
||||||
status={health?.status ?? 'unknown'}
|
size="sm"
|
||||||
latencyMs={health?.latency_ms}
|
className="w-full min-w-0 flex-nowrap border-0 p-0"
|
||||||
lastCheckedAt={health?.last_checked_at}
|
>
|
||||||
lastError={health?.last_error}
|
<ItemMedia>
|
||||||
colo={health?.colo}
|
<HealthCheckBadge
|
||||||
provider={health?.provider}
|
status={health?.status ?? 'unknown'}
|
||||||
size="xs"
|
latencyMs={health?.latency_ms}
|
||||||
/>
|
lastCheckedAt={health?.last_checked_at}
|
||||||
<TruncatedText
|
lastError={health?.last_error}
|
||||||
className={cn(
|
colo={health?.colo}
|
||||||
'min-w-0 font-mono text-xs',
|
provider={health?.provider}
|
||||||
enabled ? 'text-muted-foreground' : 'text-muted-foreground/60',
|
size="xs"
|
||||||
textClassName,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{ip}
|
|
||||||
</TruncatedText>
|
|
||||||
{copyable ? <CopyFqdnButton value={ip} /> : null}
|
|
||||||
{onToggleIp ? (
|
|
||||||
<Switch
|
|
||||||
size="sm"
|
|
||||||
className="ml-auto shrink-0"
|
|
||||||
checked={enabled}
|
|
||||||
disabled={ipToggleDisabled || togglingIp === ip}
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation()
|
|
||||||
}}
|
|
||||||
onCheckedChange={(checked) => onToggleIp(ip, Boolean(checked))}
|
|
||||||
aria-label={
|
|
||||||
enabled ? `Выключить IP ${ip}` : `Включить IP ${ip}`
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
</ItemMedia>
|
||||||
|
<ItemContent className="min-w-0 gap-0">
|
||||||
|
<div className="flex min-w-0 items-center gap-1.5">
|
||||||
|
<TruncatedText
|
||||||
|
className={cn(
|
||||||
|
'min-w-0 font-mono text-xs',
|
||||||
|
enabled
|
||||||
|
? 'text-muted-foreground'
|
||||||
|
: 'text-muted-foreground/60',
|
||||||
|
textClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{ip}
|
||||||
|
</TruncatedText>
|
||||||
|
{copyable ? <CopyFqdnButton value={ip} /> : null}
|
||||||
|
{markActive && activeSet.has(ip) ? (
|
||||||
|
<StatusBadge status="active" className="shrink-0" />
|
||||||
|
) : null}
|
||||||
|
{lbMode === 'weighted' ? (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
size="xs"
|
||||||
|
className="shrink-0 tabular-nums"
|
||||||
|
>
|
||||||
|
w{ipWeights[ip] ?? 1}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</ItemContent>
|
||||||
|
{onToggleIp ? (
|
||||||
|
<ItemActions className="ml-auto shrink-0 gap-1">
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
className="shrink-0"
|
||||||
|
checked={enabled}
|
||||||
|
disabled={ipToggleDisabled || togglingIp === ip}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
}}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onToggleIp(ip, Boolean(checked))
|
||||||
|
}
|
||||||
|
aria-label={
|
||||||
|
enabled ? `Выключить IP ${ip}` : `Включить IP ${ip}`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{showMenuSlot ? (
|
||||||
|
<span className={MENU_SLOT_CLASS} aria-hidden="true" />
|
||||||
|
) : null}
|
||||||
|
</ItemActions>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</Item>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
{extraCount > 0 ? (
|
{extraCount > 0 ? (
|
||||||
@@ -224,6 +278,6 @@ export function ServiceIpList({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</ItemGroup>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
import { Link } from '@tanstack/react-router'
|
import { Link } from '@tanstack/react-router'
|
||||||
import { MoreHorizontalIcon, ServerIcon } from 'lucide-react'
|
import {
|
||||||
|
GitForkIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
Repeat2Icon,
|
||||||
|
ScaleIcon,
|
||||||
|
ServerIcon,
|
||||||
|
type LucideIcon,
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
import { Badge } from '@/components/reui/badge'
|
import { Badge } from '@/components/reui/badge'
|
||||||
import {
|
import {
|
||||||
Frame,
|
Frame,
|
||||||
FrameDescription,
|
FrameDescription,
|
||||||
FrameHeader,
|
|
||||||
FramePanel,
|
FramePanel,
|
||||||
FrameTitle,
|
FrameTitle,
|
||||||
} from '@/components/reui/frame'
|
} from '@/components/reui/frame'
|
||||||
@@ -23,6 +29,12 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@cfdm/ui/components/dropdown-menu'
|
} from '@cfdm/ui/components/dropdown-menu'
|
||||||
|
import {
|
||||||
|
Item,
|
||||||
|
ItemActions,
|
||||||
|
ItemContent,
|
||||||
|
ItemMedia,
|
||||||
|
} from '@cfdm/ui/components/item'
|
||||||
import { Switch } from '@cfdm/ui/components/switch'
|
import { Switch } from '@cfdm/ui/components/switch'
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -30,6 +42,62 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@cfdm/ui/components/tooltip'
|
} from '@cfdm/ui/components/tooltip'
|
||||||
|
import { cn } from '@cfdm/ui/lib/utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compact service card — settings-8 DNA (Badge + copy + Switch + menu).
|
||||||
|
* Preview: https://reui.io/preview/base/settings-8
|
||||||
|
* Frame: https://reui.io/docs/components/base/frame
|
||||||
|
* IconTile: https://reui.io/docs/components/base/icon-tile
|
||||||
|
*/
|
||||||
|
|
||||||
|
type LbMode = ServiceView['lb_mode']
|
||||||
|
|
||||||
|
const LB_MODE_META: Record<
|
||||||
|
LbMode,
|
||||||
|
{ icon: LucideIcon; className: string; label: string }
|
||||||
|
> = {
|
||||||
|
round_robin: {
|
||||||
|
icon: Repeat2Icon,
|
||||||
|
className: 'text-info',
|
||||||
|
label: 'Round Robin',
|
||||||
|
},
|
||||||
|
failover: {
|
||||||
|
icon: GitForkIcon,
|
||||||
|
className: 'text-warning',
|
||||||
|
label: 'Failover (приоритет)',
|
||||||
|
},
|
||||||
|
weighted: {
|
||||||
|
icon: ScaleIcon,
|
||||||
|
className: 'text-info',
|
||||||
|
label: 'Weighted (веса)',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function LbModeTile({ mode }: { mode: LbMode }) {
|
||||||
|
const meta = LB_MODE_META[mode]
|
||||||
|
const Icon = meta.icon
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger
|
||||||
|
render={
|
||||||
|
<IconTile
|
||||||
|
variant="elevated"
|
||||||
|
size="xs"
|
||||||
|
className={cn('shrink-0', meta.className)}
|
||||||
|
aria-label={meta.label}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon aria-hidden="true" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{meta.label}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
interface ServiceUnitCardProps {
|
interface ServiceUnitCardProps {
|
||||||
service: ServiceView
|
service: ServiceView
|
||||||
@@ -55,27 +123,32 @@ export function ServiceUnitCard({
|
|||||||
const extraCount = Math.max(0, fqdns.length - 1)
|
const extraCount = Math.max(0, fqdns.length - 1)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Frame dense spacing="sm" className="h-full min-w-0 overflow-hidden">
|
<Frame stacked spacing="sm" className="h-full min-w-0">
|
||||||
<FrameHeader className="flex-row items-center justify-between gap-2">
|
<FramePanel fit>
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
<Item size="sm" className="w-full min-w-0 flex-nowrap border-0 p-0">
|
||||||
<IconTile
|
<ItemMedia>
|
||||||
variant="elevated"
|
<IconTile
|
||||||
size="sm"
|
variant="elevated"
|
||||||
className="text-muted-foreground"
|
size="sm"
|
||||||
aria-hidden="true"
|
className="text-foreground"
|
||||||
>
|
aria-hidden="true"
|
||||||
<ServerIcon />
|
>
|
||||||
</IconTile>
|
<ServerIcon />
|
||||||
<div className="flex min-w-0 flex-col gap-px">
|
</IconTile>
|
||||||
<FrameTitle className="min-w-0 truncate text-sm">
|
</ItemMedia>
|
||||||
<Link
|
<ItemContent className="min-w-0 gap-px">
|
||||||
to="/services/$serviceId"
|
<div className="flex min-w-0 items-center gap-1.5">
|
||||||
params={{ serviceId: String(service.id) }}
|
<FrameTitle className="min-w-0 truncate text-base font-semibold">
|
||||||
className="hover:underline"
|
<Link
|
||||||
>
|
to="/services/$serviceId"
|
||||||
{service.name}
|
params={{ serviceId: String(service.id) }}
|
||||||
</Link>
|
className="hover:underline"
|
||||||
</FrameTitle>
|
>
|
||||||
|
{service.name}
|
||||||
|
</Link>
|
||||||
|
</FrameTitle>
|
||||||
|
<LbModeTile mode={service.lb_mode} />
|
||||||
|
</div>
|
||||||
<div className="flex min-w-0 items-center gap-1">
|
<div className="flex min-w-0 items-center gap-1">
|
||||||
<FrameDescription className="min-w-0 truncate font-mono text-xs">
|
<FrameDescription className="min-w-0 truncate font-mono text-xs">
|
||||||
{primaryDomain}
|
{primaryDomain}
|
||||||
@@ -108,66 +181,70 @@ export function ServiceUnitCard({
|
|||||||
<CopyFqdnButton value={fqdns.join('\n')} />
|
<CopyFqdnButton value={fqdns.join('\n')} />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ItemContent>
|
||||||
</div>
|
<ItemActions className="ml-auto shrink-0 gap-1">
|
||||||
<div className="flex shrink-0 items-center gap-1">
|
<Switch
|
||||||
<Switch
|
size="sm"
|
||||||
size="sm"
|
checked={service.enabled}
|
||||||
checked={service.enabled}
|
disabled={togglingId === service.id}
|
||||||
disabled={togglingId === service.id}
|
onCheckedChange={(checked) =>
|
||||||
onCheckedChange={(checked) =>
|
onToggleService(service.id, Boolean(checked))
|
||||||
onToggleService(service.id, Boolean(checked))
|
|
||||||
}
|
|
||||||
aria-label={
|
|
||||||
service.enabled ? 'Выключить сервис' : 'Включить сервис'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger
|
|
||||||
render={
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon-sm"
|
|
||||||
aria-label={`Действия ${service.name}`}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
>
|
aria-label={
|
||||||
<MoreHorizontalIcon aria-hidden />
|
service.enabled ? 'Выключить сервис' : 'Включить сервис'
|
||||||
</DropdownMenuTrigger>
|
}
|
||||||
<DropdownMenuContent align="end">
|
/>
|
||||||
<DropdownMenuItem
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
render={
|
render={
|
||||||
<Link
|
<Button
|
||||||
to="/services/$serviceId"
|
type="button"
|
||||||
params={{ serviceId: String(service.id) }}
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
aria-label={`Действия ${service.name}`}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Обзор
|
<MoreHorizontalIcon aria-hidden />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuItem onClick={() => onEditService(service)}>
|
<DropdownMenuContent align="end">
|
||||||
Изменить
|
<DropdownMenuItem
|
||||||
</DropdownMenuItem>
|
render={
|
||||||
<DropdownMenuItem
|
<Link
|
||||||
variant="destructive"
|
to="/services/$serviceId"
|
||||||
onClick={() => onDeleteService(service)}
|
params={{ serviceId: String(service.id) }}
|
||||||
>
|
/>
|
||||||
Удалить
|
}
|
||||||
</DropdownMenuItem>
|
>
|
||||||
</DropdownMenuContent>
|
Обзор
|
||||||
</DropdownMenu>
|
</DropdownMenuItem>
|
||||||
</div>
|
<DropdownMenuItem onClick={() => onEditService(service)}>
|
||||||
</FrameHeader>
|
Изменить
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => onDeleteService(service)}
|
||||||
|
>
|
||||||
|
Удалить
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</ItemActions>
|
||||||
|
</Item>
|
||||||
|
</FramePanel>
|
||||||
|
|
||||||
<FramePanel className="flex min-w-0 flex-col gap-1 pt-0 shadow-none!">
|
<FramePanel className="flex min-w-0 flex-col">
|
||||||
<ServiceIpList
|
<ServiceIpList
|
||||||
copyable
|
copyable
|
||||||
|
alignWithMenu
|
||||||
ips={service.ips ?? []}
|
ips={service.ips ?? []}
|
||||||
ipHealth={service.ip_health ?? []}
|
ipHealth={service.ip_health ?? []}
|
||||||
ipEnabled={service.ip_enabled ?? {}}
|
ipEnabled={service.ip_enabled ?? {}}
|
||||||
ipToggleDisabled={togglingId === service.id}
|
ipToggleDisabled={togglingId === service.id}
|
||||||
togglingIp={togglingIp}
|
togglingIp={togglingIp}
|
||||||
|
lbMode={service.lb_mode}
|
||||||
|
activeIps={service.active_ips}
|
||||||
|
ipWeights={service.domains[0]?.target_ip_weights}
|
||||||
onToggleIp={(ip, enabled) =>
|
onToggleIp={(ip, enabled) =>
|
||||||
onToggleServiceIp(service.id, ip, enabled)
|
onToggleServiceIp(service.id, ip, enabled)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,8 @@ export const serviceViewSchema = serviceSchema.extend({
|
|||||||
health_latency_ms: z.number().nullable().default(null),
|
health_latency_ms: z.number().nullable().default(null),
|
||||||
ip_health: z.array(serviceIpHealthSchema).default([]),
|
ip_health: z.array(serviceIpHealthSchema).default([]),
|
||||||
ip_enabled: z.record(z.string(), z.boolean()).default({}),
|
ip_enabled: z.record(z.string(), z.boolean()).default({}),
|
||||||
|
lb_mode: z.enum(['round_robin', 'failover', 'weighted']).catch('round_robin'),
|
||||||
|
active_ips: z.array(z.string()).default([]),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const serviceGroupViewSchema = serviceGroupSchema.extend({
|
export const serviceGroupViewSchema = serviceGroupSchema.extend({
|
||||||
|
|||||||
Vendored
+26
@@ -176,6 +176,8 @@ interface ServiceView$1 {
|
|||||||
health_latency_ms: number | null;
|
health_latency_ms: number | null;
|
||||||
ip_health: ServiceIpHealth$1[];
|
ip_health: ServiceIpHealth$1[];
|
||||||
ip_enabled: Record<string, boolean>;
|
ip_enabled: Record<string, boolean>;
|
||||||
|
lb_mode: LbMode;
|
||||||
|
active_ips: string[];
|
||||||
}
|
}
|
||||||
interface SyncJob {
|
interface SyncJob {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -824,6 +826,12 @@ declare const serviceViewSchema: z.ZodObject<{
|
|||||||
colo: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
colo: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||||
}, z.core.$strip>>>;
|
}, z.core.$strip>>>;
|
||||||
ip_enabled: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
ip_enabled: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
||||||
|
lb_mode: z.ZodCatch<z.ZodEnum<{
|
||||||
|
round_robin: "round_robin";
|
||||||
|
failover: "failover";
|
||||||
|
weighted: "weighted";
|
||||||
|
}>>;
|
||||||
|
active_ips: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
||||||
}, z.core.$strip>;
|
}, z.core.$strip>;
|
||||||
declare const serviceGroupViewSchema: z.ZodObject<{
|
declare const serviceGroupViewSchema: z.ZodObject<{
|
||||||
id: z.ZodNumber;
|
id: z.ZodNumber;
|
||||||
@@ -1013,6 +1021,12 @@ declare const serviceGroupViewSchema: z.ZodObject<{
|
|||||||
colo: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
colo: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||||
}, z.core.$strip>>>;
|
}, z.core.$strip>>>;
|
||||||
ip_enabled: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
ip_enabled: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
||||||
|
lb_mode: z.ZodCatch<z.ZodEnum<{
|
||||||
|
round_robin: "round_robin";
|
||||||
|
failover: "failover";
|
||||||
|
weighted: "weighted";
|
||||||
|
}>>;
|
||||||
|
active_ips: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
||||||
}, z.core.$strip>>>;
|
}, z.core.$strip>>>;
|
||||||
health_status: z.ZodDefault<z.ZodEnum<{
|
health_status: z.ZodDefault<z.ZodEnum<{
|
||||||
unknown: "unknown";
|
unknown: "unknown";
|
||||||
@@ -1211,6 +1225,12 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
|
|||||||
colo: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
colo: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||||
}, z.core.$strip>>>;
|
}, z.core.$strip>>>;
|
||||||
ip_enabled: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
ip_enabled: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
||||||
|
lb_mode: z.ZodCatch<z.ZodEnum<{
|
||||||
|
round_robin: "round_robin";
|
||||||
|
failover: "failover";
|
||||||
|
weighted: "weighted";
|
||||||
|
}>>;
|
||||||
|
active_ips: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
||||||
}, z.core.$strip>>>;
|
}, z.core.$strip>>>;
|
||||||
health_status: z.ZodDefault<z.ZodEnum<{
|
health_status: z.ZodDefault<z.ZodEnum<{
|
||||||
unknown: "unknown";
|
unknown: "unknown";
|
||||||
@@ -1360,6 +1380,12 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
|
|||||||
colo: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
colo: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||||
}, z.core.$strip>>>;
|
}, z.core.$strip>>>;
|
||||||
ip_enabled: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
ip_enabled: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
||||||
|
lb_mode: z.ZodCatch<z.ZodEnum<{
|
||||||
|
round_robin: "round_robin";
|
||||||
|
failover: "failover";
|
||||||
|
weighted: "weighted";
|
||||||
|
}>>;
|
||||||
|
active_ips: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
||||||
}, z.core.$strip>>>;
|
}, z.core.$strip>>>;
|
||||||
}, z.core.$strip>;
|
}, z.core.$strip>;
|
||||||
declare const domainSchema: z.ZodObject<{
|
declare const domainSchema: z.ZodObject<{
|
||||||
|
|||||||
Vendored
+3
-1
@@ -409,7 +409,9 @@ var serviceViewSchema = serviceSchema.extend({
|
|||||||
health_status: ipHealthStateSchema.default("unknown"),
|
health_status: ipHealthStateSchema.default("unknown"),
|
||||||
health_latency_ms: z.number().nullable().default(null),
|
health_latency_ms: z.number().nullable().default(null),
|
||||||
ip_health: z.array(serviceIpHealthSchema).default([]),
|
ip_health: z.array(serviceIpHealthSchema).default([]),
|
||||||
ip_enabled: z.record(z.string(), z.boolean()).default({})
|
ip_enabled: z.record(z.string(), z.boolean()).default({}),
|
||||||
|
lb_mode: lbModeSchema.catch("round_robin"),
|
||||||
|
active_ips: z.array(z.string()).default([])
|
||||||
});
|
});
|
||||||
var serviceGroupViewSchema = serviceGroupSchema.extend({
|
var serviceGroupViewSchema = serviceGroupSchema.extend({
|
||||||
services: z.array(serviceViewSchema).default([]),
|
services: z.array(serviceViewSchema).default([]),
|
||||||
|
|||||||
@@ -205,6 +205,8 @@ export const serviceViewSchema = serviceSchema.extend({
|
|||||||
health_latency_ms: z.number().nullable().default(null),
|
health_latency_ms: z.number().nullable().default(null),
|
||||||
ip_health: z.array(serviceIpHealthSchema).default([]),
|
ip_health: z.array(serviceIpHealthSchema).default([]),
|
||||||
ip_enabled: z.record(z.string(), z.boolean()).default({}),
|
ip_enabled: z.record(z.string(), z.boolean()).default({}),
|
||||||
|
lb_mode: lbModeSchema.catch('round_robin'),
|
||||||
|
active_ips: z.array(z.string()).default([]),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const serviceGroupViewSchema = serviceGroupSchema.extend({
|
export const serviceGroupViewSchema = serviceGroupSchema.extend({
|
||||||
|
|||||||
@@ -222,6 +222,8 @@ export interface ServiceView {
|
|||||||
health_latency_ms: number | null;
|
health_latency_ms: number | null;
|
||||||
ip_health: ServiceIpHealth[];
|
ip_health: ServiceIpHealth[];
|
||||||
ip_enabled: Record<string, boolean>;
|
ip_enabled: Record<string, boolean>;
|
||||||
|
lb_mode: LbMode;
|
||||||
|
active_ips: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupWithStats extends Group {
|
export interface GroupWithStats extends Group {
|
||||||
|
|||||||
Reference in new issue
Block a user